From fdb4c1ce97a302d2fee81f372c79f114ce967a96 Mon Sep 17 00:00:00 2001 From: crystall-bitquill <97126568+crystall-bitquill@users.noreply.github.com> Date: Thu, 31 Oct 2024 09:47:16 -0700 Subject: [PATCH] fix: cn and gov endpoint support (#244) --- .../rds_host_list_provider.ts | 22 +- common/lib/utils/connection_url_parser.ts | 1 - common/lib/utils/rds_utils.ts | 233 +++++++++++++----- common/lib/utils/utils.ts | 4 + tests/unit/rds_host_list_provider.test.ts | 51 ++-- tests/unit/rds_utils.test.ts | 210 ++++++++++++++-- 6 files changed, 385 insertions(+), 136 deletions(-) diff --git a/common/lib/host_list_provider/rds_host_list_provider.ts b/common/lib/host_list_provider/rds_host_list_provider.ts index 90d57d0d..0be43fa7 100644 --- a/common/lib/host_list_provider/rds_host_list_provider.ts +++ b/common/lib/host_list_provider/rds_host_list_provider.ts @@ -77,7 +77,7 @@ export class RdsHostListProvider implements DynamicHostListProvider { this.initialHost = this.initialHostList[0]; this.hostListProviderService.setInitialConnectionHostInfo(this.initialHost); this.refreshRateNano = WrapperProperties.CLUSTER_TOPOLOGY_REFRESH_RATE_MS.get(this.properties) * 1000000; - this.rdsUrlType = this.rdsHelper.identifyRdsType(this.originalUrl); + this.rdsUrlType = this.rdsHelper.identifyRdsType(this.initialHost.host); } init(): void { @@ -104,12 +104,12 @@ export class RdsHostListProvider implements DynamicHostListProvider { // identification this.clusterId = this.initialHost.url; } else if (this.rdsUrlType.isRds) { - const clusterSuggestedResult: ClusterSuggestedResult | null = this.getSuggestedClusterId(this.initialHost.url); + const clusterSuggestedResult: ClusterSuggestedResult | null = this.getSuggestedClusterId(this.initialHost.host); if (clusterSuggestedResult && clusterSuggestedResult.clusterId) { this.clusterId = clusterSuggestedResult.clusterId; this.isPrimaryClusterId = clusterSuggestedResult.isPrimaryClusterId; } else { - const clusterRdsHostUrl: string | null = this.rdsHelper.getRdsClusterHostUrl(this.initialHost.url); + const clusterRdsHostUrl: string | null = this.rdsHelper.getRdsClusterHostUrl(this.initialHost.host); if (clusterRdsHostUrl) { this.clusterId = this.clusterInstanceTemplate.isPortSpecified() ? `${clusterRdsHostUrl}:${this.clusterInstanceTemplate.port}` @@ -143,7 +143,7 @@ export class RdsHostListProvider implements DynamicHostListProvider { } if (client.targetClient) { - return dialect.getHostRole(client.targetClient, this.properties); + return dialect.getHostRole(client.targetClient); } else { throw new AwsWrapperError(Messages.get("AwsClient targetClient not defined.")); } @@ -153,7 +153,7 @@ export class RdsHostListProvider implements DynamicHostListProvider { if (!this.isTopologyAwareDatabaseDialect(dialect)) { throw new TypeError(Messages.get("RdsHostListProvider.incorrectDialect")); } - const instanceName = await dialect.identifyConnection(targetClient, this.properties); + const instanceName = await dialect.identifyConnection(targetClient); return this.refresh(targetClient).then((topology) => { const matches = topology.filter((host) => host.hostId === instanceName); @@ -215,17 +215,17 @@ export class RdsHostListProvider implements DynamicHostListProvider { } } - private getSuggestedClusterId(url: string): ClusterSuggestedResult | null { + private getSuggestedClusterId(host: string): ClusterSuggestedResult | null { for (const [key, hosts] of RdsHostListProvider.topologyCache.getEntries()) { const isPrimaryCluster: boolean = RdsHostListProvider.primaryClusterIdCache.get(key, false, this.suggestedClusterIdRefreshRateNano) ?? false; - if (key === url) { - return new ClusterSuggestedResult(url, isPrimaryCluster); + if (key === host) { + return new ClusterSuggestedResult(host, isPrimaryCluster); } if (hosts) { - for (const host of hosts) { - if (host.url === url) { - logger.debug(Messages.get("RdsHostListProvider.suggestedClusterId", key, url)); + for (const hostInfo of hosts) { + if (hostInfo.host === host) { + logger.debug(Messages.get("RdsHostListProvider.suggestedClusterId", key, host)); return new ClusterSuggestedResult(key, isPrimaryCluster); } } diff --git a/common/lib/utils/connection_url_parser.ts b/common/lib/utils/connection_url_parser.ts index a03ee413..77e417a8 100644 --- a/common/lib/utils/connection_url_parser.ts +++ b/common/lib/utils/connection_url_parser.ts @@ -35,7 +35,6 @@ export abstract class ConnectionUrlParser { private getHostInfo(host: string, port: string | undefined, role: HostRole, builder: HostInfoBuilder): HostInfo { const hostId = ConnectionUrlParser.rdsUtils.getRdsInstanceId(host); - builder = builder.withHost(host).withRole(role); if (hostId) { diff --git a/common/lib/utils/rds_utils.ts b/common/lib/utils/rds_utils.ts index a9d2ac59..8a5d83ac 100644 --- a/common/lib/utils/rds_utils.ts +++ b/common/lib/utils/rds_utils.ts @@ -15,6 +15,7 @@ */ import { RdsUrlType } from "./rds_url_type"; +import { equalsIgnoreCase } from "./utils"; export class RdsUtils { // Aurora DB clusters support different endpoints. More details about Aurora RDS endpoints @@ -53,27 +54,49 @@ export class RdsUtils { // // Instance Endpoint: ..rds..amazonaws.com.cn // Example: test-postgres-instance-1.123456789012.rds.cn-northwest-1.amazonaws.com.cn + // + // + // Governmental endpoints + // https://aws.amazon.com/compliance/fips/#FIPS_Endpoints_by_Service + // https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/model/Region.html private static readonly AURORA_DNS_PATTERN = - /(?.+)\.(?proxy-|cluster-|cluster-ro-|cluster-custom-)?(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)/i; - private static readonly AURORA_INSTANCE_PATTERN = /(?.+)\.(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)/i; + /^(?.+)\.(?proxy-|cluster-|cluster-ro-|cluster-custom-)?(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)$/i; + private static readonly AURORA_INSTANCE_PATTERN = /^(?.+)\.(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)$/i; private static readonly AURORA_CLUSTER_PATTERN = - /(?.+)\.(?cluster-|cluster-ro-)+(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)/i; + /^(?.+)\.(?cluster-|cluster-ro-)+(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)$/i; private static readonly AURORA_CUSTOM_CLUSTER_PATTERN = - /(?.+)\.(?cluster-custom-)+(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)/i; + /^(?.+)\.(?cluster-custom-)+(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)$/i; private static readonly AURORA_PROXY_DNS_PATTERN = - /(?.+)\.(?proxy-)+(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)/i; + /^(?.+)\.(?proxy-)+(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)$/i; private static readonly AURORA_CHINA_DNS_PATTERN = - /(?.+)\.(?proxy-|cluster-|cluster-ro-|cluster-custom-)?(?[a-zA-Z0-9]+\.rds\.(?[a-zA-Z0-9-]+)\.amazonaws\.com\.cn)/i; + /^(?.+)\.(?proxy-|cluster-|cluster-ro-|cluster-custom-)?(?[a-zA-Z0-9]+\.rds\.(?[a-zA-Z0-9-]+)\.amazonaws\.com\.cn)$/i; + private static readonly AURORA_OLD_CHINA_DNS_PATTERN = + /^(?.+)\.(?proxy-|cluster-|cluster-ro-|cluster-custom-)?(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com\.cn)$/i; private static readonly AURORA_CHINA_INSTANCE_PATTERN = - /(?.+)\.(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com\.cn)/i; + /^(?.+)\.(?[a-zA-Z0-9]+\.rds\.(?[a-zA-Z0-9-]+)\.amazonaws\.com\.cn)$/i; + private static readonly AURORA_OLD_CHINA_INSTANCE_PATTERN = + /^(?.+)\.(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com\.cn)$/i; private static readonly AURORA_CHINA_CLUSTER_PATTERN = - /(?.+)\.(?cluster-|cluster-ro-)+(?[a-zA-Z0-9]+\.rds\.(?[a-zA-Z0-9-]+)\.amazonaws\.com\.cn)/i; + /^(?.+)\.(?cluster-|cluster-ro-)+(?[a-zA-Z0-9]+\.rds\.(?[a-zA-Z0-9-]+)\.amazonaws\.com\.cn)$/i; + private static readonly AURORA_OLD_CHINA_CLUSTER_PATTERN = + /^(?.+)\.(?cluster-|cluster-ro-)+(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com\.cn)$/i; private static readonly AURORA_CHINA_CUSTOM_CLUSTER_PATTERN = - /(?.+)\.(?cluster-custom-)+(?[a-zA-Z0-9]+\.rds\.(?[a-zA-Z0-9-]+)\.amazonaws\.com\.cn)/i; + /^(?.+)\.(?cluster-custom-)+(?[a-zA-Z0-9]+\.rds\.(?[a-zA-Z0-9-]+)\.amazonaws\.com\.cn)$/i; + private static readonly AURORA_OLD_CHINA_CUSTOM_CLUSTER_PATTERN = + /^(?.+)\.(?cluster-custom-)+(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-]+)\.rds\.amazonaws\.com\.cn)$/i; private static readonly AURORA_CHINA_PROXY_DNS_PATTERN = - /(?.+)\.(?proxy-)+(?[a-zA-Z0-9]+\.rds\.(?[a-zA-Z0-9-])+\.amazonaws\.com\.cn)/i; - private static readonly ELB_PATTERN = /(?.+)\.elb\.((?[a-zA-Z0-9-]+)\.amazonaws\.com)/i; + /^(?.+)\.(?proxy-)+(?[a-zA-Z0-9]+\.rds\.(?[a-zA-Z0-9-])+\.amazonaws\.com\.cn)$/i; + private static readonly AURORA_OLD_CHINA_PROXY_DNS_PATTERN = + /^(?.+)\.(?proxy-)+(?[a-zA-Z0-9]+\.(?[a-zA-Z0-9-])+\.rds\.amazonaws\.com\.cn)$/i; + + private static readonly AURORA_GOV_DNS_PATTERN = + /^(?.+)\.(?proxy-|cluster-|cluster-ro-|cluster-custom-|shardgrp-)?(?[a-zA-Z0-9]+\.rds\.(?[a-zA-Z0-9-]+)\.(amazonaws\.com|c2s\.ic\.gov|sc2s\.sgov\.gov))$/i; + + private static readonly AURORA_GOV_CLUSTER_PATTERN = + /^(?.+)\.(?cluster-|cluster-ro-)+(?[a-zA-Z0-9]+\.rds\.(?[a-zA-Z0-9-]+)\.(amazonaws\.com|c2s\.ic\.gov|sc2s\.sgov\.gov))$/i; + + private static readonly ELB_PATTERN = /^(?.+)\.elb\.((?[a-zA-Z0-9-]+)\.amazonaws\.com)$/i; private static readonly IP_V4 = /^(([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){1}(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){2}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/i; private static readonly IP_V6 = /^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){7}$/i; @@ -85,28 +108,43 @@ export class RdsUtils { static readonly DOMAIN_GROUP = "domain"; static readonly REGION_GROUP = "region"; - public isRdsClusterDns(host: string): boolean | null { - return host.match(RdsUtils.AURORA_CLUSTER_PATTERN) != null || host.match(RdsUtils.AURORA_CHINA_CLUSTER_PATTERN) != null; + private static readonly cachedPatterns = new Map(); + private static readonly cachedDnsPatterns = new Map(); + + public isRdsClusterDns(host: string): boolean { + const dnsGroup = this.getDnsGroup(host); + return equalsIgnoreCase(dnsGroup, "cluster-") || equalsIgnoreCase(dnsGroup, "cluster-ro-"); } public isRdsCustomClusterDns(host: string): boolean { - return host.match(RdsUtils.AURORA_CUSTOM_CLUSTER_PATTERN) != null || host.match(RdsUtils.AURORA_CHINA_CUSTOM_CLUSTER_PATTERN) != null; + const dnsGroup = this.getDnsGroup(host); + return equalsIgnoreCase(dnsGroup, "cluster-custom-"); } public isRdsDns(host: string) { - return host.match(RdsUtils.AURORA_DNS_PATTERN) || host.match(RdsUtils.AURORA_CHINA_DNS_PATTERN); + const matcher = this.cacheMatcher( + host, + RdsUtils.AURORA_DNS_PATTERN, + RdsUtils.AURORA_CHINA_DNS_PATTERN, + RdsUtils.AURORA_OLD_CHINA_DNS_PATTERN, + RdsUtils.AURORA_GOV_DNS_PATTERN + ); + const group = this.getRegexGroup(matcher, RdsUtils.DNS_GROUP); + + if (group) { + RdsUtils.cachedDnsPatterns.set(host, group); + } + + return matcher != null; } public isRdsInstance(host: string): boolean { - return host.match(RdsUtils.AURORA_INSTANCE_PATTERN) !== null || host.match(RdsUtils.AURORA_CHINA_INSTANCE_PATTERN) !== null; + return !this.getDnsGroup(host) && this.isRdsDns(host); } isRdsProxyDns(host: string) { - return host.match(RdsUtils.AURORA_PROXY_DNS_PATTERN) || host.match(RdsUtils.AURORA_CHINA_PROXY_DNS_PATTERN); - } - - public isElbUrl(host: string) { - return host.match(RdsUtils.ELB_PATTERN); + const dnsGroup = this.getDnsGroup(host); + return dnsGroup && dnsGroup.startsWith("proxy-"); } public getRdsInstanceId(host: string) { @@ -114,80 +152,92 @@ export class RdsUtils { return null; } - const instanceId = (host.match(RdsUtils.AURORA_INSTANCE_PATTERN) || host.match(RdsUtils.AURORA_CHINA_INSTANCE_PATTERN))?.groups?.[ - RdsUtils.INSTANCE_GROUP - ]; - return instanceId ? instanceId : null; + const matcher = this.cacheMatcher( + host, + RdsUtils.AURORA_DNS_PATTERN, + RdsUtils.AURORA_CHINA_DNS_PATTERN, + RdsUtils.AURORA_OLD_CHINA_DNS_PATTERN, + RdsUtils.AURORA_GOV_DNS_PATTERN + ); + if (this.getRegexGroup(matcher, RdsUtils.DNS_GROUP)) { + return this.getRegexGroup(matcher, RdsUtils.INSTANCE_GROUP); + } + + return null; } public getRdsInstanceHostPattern(host: string): string { - if (host == null) { + if (!host) { return "?"; } - const matcher = host.match(RdsUtils.AURORA_DNS_PATTERN); - if (matcher !== null && matcher.groups !== undefined) { - return "?." + matcher.groups[RdsUtils.DOMAIN_GROUP]; - } - const chinaMatcher = host.match(RdsUtils.AURORA_CHINA_DNS_PATTERN); - if (chinaMatcher !== null && chinaMatcher.groups !== undefined) { - return "?." + chinaMatcher.groups[RdsUtils.DOMAIN_GROUP]; - } - return "?"; + const matcher = this.cacheMatcher( + host, + RdsUtils.AURORA_DNS_PATTERN, + RdsUtils.AURORA_CHINA_DNS_PATTERN, + RdsUtils.AURORA_OLD_CHINA_DNS_PATTERN, + RdsUtils.AURORA_GOV_DNS_PATTERN + ); + const group = this.getRegexGroup(matcher, RdsUtils.DOMAIN_GROUP); + return group ? `?.${group}` : "?"; } public getRdsRegion(host: string): string | null { - const matcher = host.match(RdsUtils.AURORA_DNS_PATTERN); - if (matcher !== null && matcher.groups !== undefined) { - return matcher.groups[RdsUtils.REGION_GROUP]; + if (!host) { + return null; } - const chinaMatcher = host.match(RdsUtils.AURORA_CHINA_DNS_PATTERN); - if (chinaMatcher !== null && chinaMatcher.groups !== undefined) { - return chinaMatcher.groups[RdsUtils.REGION_GROUP]; + + const matcher = this.cacheMatcher( + host, + RdsUtils.AURORA_DNS_PATTERN, + RdsUtils.AURORA_CHINA_DNS_PATTERN, + RdsUtils.AURORA_OLD_CHINA_DNS_PATTERN, + RdsUtils.AURORA_GOV_DNS_PATTERN + ); + + const group = this.getRegexGroup(matcher, RdsUtils.REGION_GROUP); + if (group) { + return group; } + const elbMatcher = host.match(RdsUtils.ELB_PATTERN); - if (elbMatcher !== null && elbMatcher.groups !== undefined) { - return elbMatcher.groups[RdsUtils.REGION_GROUP]; + if (elbMatcher && elbMatcher.length > 0) { + return this.getRegexGroup(elbMatcher, RdsUtils.REGION_GROUP); } + return null; } - public isWriterClusterDns(host: string) { - if (host === undefined) { - return false; - } - - const matcher = host.match(RdsUtils.AURORA_CLUSTER_PATTERN); - if (matcher !== null && matcher.groups !== undefined) { - return "cluster-" === matcher.groups[RdsUtils.DNS_GROUP]; - } - const chinaMatcher = host.match(RdsUtils.AURORA_CHINA_CLUSTER_PATTERN); - if (chinaMatcher !== null && chinaMatcher.groups !== undefined) { - return "cluster-" == chinaMatcher.groups[RdsUtils.DNS_GROUP]; - } - return false; + public isWriterClusterDns(host: string): boolean { + const dnsGroup = this.getDnsGroup(host); + return equalsIgnoreCase(dnsGroup, "cluster-"); } public isReaderClusterDns(host: string): boolean { - const matcher = host.match(RdsUtils.AURORA_CLUSTER_PATTERN); - if (matcher !== null && matcher.groups !== undefined) { - return "cluster-ro-" == matcher.groups[RdsUtils.DNS_GROUP]; - } - const chinaMatcher = host.match(RdsUtils.AURORA_CHINA_CLUSTER_PATTERN); - if (chinaMatcher !== null && chinaMatcher.groups !== undefined) { - return "cluster-ro-" == chinaMatcher.groups[RdsUtils.DNS_GROUP]; - } - return false; + const dnsGroup = this.getDnsGroup(host); + return equalsIgnoreCase(dnsGroup, "cluster-ro-"); } public getRdsClusterHostUrl(host: string): string | null { + if (!host) { + return null; + } + const matcher = host.match(RdsUtils.AURORA_CLUSTER_PATTERN); if (matcher) { return host.replace(RdsUtils.AURORA_CLUSTER_PATTERN, "$.cluster-$"); } const chinaMatcher = host.match(RdsUtils.AURORA_CHINA_CLUSTER_PATTERN); if (chinaMatcher) { - return host.replace(RdsUtils.AURORA_CHINA_CLUSTER_PATTERN, "${.cluster-$"); + return host.replace(RdsUtils.AURORA_CHINA_CLUSTER_PATTERN, "$.cluster-$"); + } + const oldChinaMatcher = host.match(RdsUtils.AURORA_OLD_CHINA_CLUSTER_PATTERN); + if (oldChinaMatcher) { + return host.replace(RdsUtils.AURORA_OLD_CHINA_CLUSTER_PATTERN, "$.cluster-$"); + } + const govMatcher = host.match(RdsUtils.AURORA_GOV_CLUSTER_PATTERN); + if (govMatcher) { + return host.replace(RdsUtils.AURORA_GOV_CLUSTER_PATTERN, "$.cluster-$"); } return null; } @@ -259,4 +309,53 @@ export class RdsUtils { } return hostAndPort.substring(0, index); } + + private getDnsGroup(host: string): string | null { + if (!host) { + return null; + } + + const group = RdsUtils.cachedDnsPatterns.get(host); + if (group) { + return group; + } + + const matcher = this.cacheMatcher( + host, + RdsUtils.AURORA_DNS_PATTERN, + RdsUtils.AURORA_CHINA_DNS_PATTERN, + RdsUtils.AURORA_OLD_CHINA_DNS_PATTERN, + RdsUtils.AURORA_GOV_DNS_PATTERN + ); + return this.getRegexGroup(matcher, RdsUtils.DNS_GROUP); + } + + private getRegexGroup(matcher: RegExpMatchArray, groupName: string): string | null { + if (!matcher) { + return null; + } + + return matcher.groups?.[groupName] ?? null; + } + + private cacheMatcher(host: string, ...patterns: RegExp[]) { + let matcher = null; + for (const pattern of patterns) { + matcher = RdsUtils.cachedPatterns.get(host); + if (matcher) { + return matcher; + } + matcher = host.match(pattern); + if (matcher && matcher.length > 0) { + RdsUtils.cachedPatterns.set(host, matcher); + return matcher; + } + } + return null; + } + + static clearCache() { + RdsUtils.cachedPatterns.clear(); + RdsUtils.cachedDnsPatterns.clear(); + } } diff --git a/common/lib/utils/utils.ts b/common/lib/utils/utils.ts index d45e6af1..93158174 100644 --- a/common/lib/utils/utils.ts +++ b/common/lib/utils/utils.ts @@ -72,3 +72,7 @@ export function logAndThrowError(message: string) { logger.error(message); throw new AwsWrapperError(message); } + +export function equalsIgnoreCase(value1: string | null, value2: string | null): boolean { + return value1 != null && value2 != null && value1.localeCompare(value2, undefined, { sensitivity: "accent" }) === 0; +} diff --git a/tests/unit/rds_host_list_provider.test.ts b/tests/unit/rds_host_list_provider.test.ts index 4f79b1ba..156a5627 100644 --- a/tests/unit/rds_host_list_provider.test.ts +++ b/tests/unit/rds_host_list_provider.test.ts @@ -15,7 +15,7 @@ */ import { RdsHostListProvider } from "../../common/lib/host_list_provider/rds_host_list_provider"; -import { anyFunction, anyString, anything, instance, mock, reset, spy, verify, when } from "ts-mockito"; +import { anything, instance, mock, reset, spy, verify, when } from "ts-mockito"; import { PluginService } from "../../common/lib/plugin_service"; import { AwsClient } from "../../common/lib/aws_client"; import { HostInfo } from "../../common/lib/host_info"; @@ -28,11 +28,13 @@ import { sleep } from "../../common/lib/utils/utils"; import { HostRole } from "../../common/lib/host_role"; import { AuroraPgDatabaseDialect } from "../../pg/lib/dialect/aurora_pg_database_dialect"; import { ClientWrapper } from "../../common/lib/client_wrapper"; +import { PgConnectionUrlParser } from "../../pg/lib/pg_connection_url_parser"; +import { PgClientWrapper } from "../../common/lib/pg_client_wrapper"; const mockClient: AwsClient = mock(AwsPGClient); const mockDialect: AuroraPgDatabaseDialect = mock(AuroraPgDatabaseDialect); const mockPluginService: PluginService = mock(PluginService); -const mockConnectionUrlParser: ConnectionUrlParser = mock(ConnectionUrlParser); +const connectionUrlParser: ConnectionUrlParser = new PgConnectionUrlParser(); const updateTime: number = Date.now(); const hosts: HostInfo[] = [ @@ -52,11 +54,7 @@ const currentHostInfo = createHost({ hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy() }); -const clientWrapper: ClientWrapper = { - client: undefined, - hostInfo: currentHostInfo, - properties: new Map() -}; +const clientWrapper: ClientWrapper = new PgClientWrapper(undefined, currentHostInfo, new Map()); const mockClientWrapper: ClientWrapper = mock(clientWrapper); @@ -75,7 +73,6 @@ function getRdsHostListProvider(originalHost: string): RdsHostListProvider { host: originalHost }) ]; - when(mockConnectionUrlParser.getHostsFromConnectionUrl(anyString(), anything(), anything(), anyFunction())).thenReturn(host); const provider = new RdsHostListProvider(new Map(), originalHost, instance(mockPluginService)); provider.init(); @@ -88,7 +85,7 @@ describe("testRdsHostListProvider", () => { when(mockClient.isValid()).thenResolve(true); when(mockPluginService.getDialect()).thenReturn(instance(mockDialect)); when(mockPluginService.getCurrentHostInfo()).thenReturn(currentHostInfo); - when(mockPluginService.getConnectionUrlParser()).thenReturn(instance(mockConnectionUrlParser)); + when(mockPluginService.getConnectionUrlParser()).thenReturn(connectionUrlParser); when(mockPluginService.getCurrentClient()).thenReturn(instance(mockClient)); when(mockPluginService.getHostInfoBuilder()).thenReturn(new HostInfoBuilder({ hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy() })); }); @@ -164,7 +161,6 @@ describe("testRdsHostListProvider", () => { host: "someUrl" }) ]; - when(mockConnectionUrlParser.getHostsFromConnectionUrl(anyString(), anything(), anything(), anyFunction())).thenReturn(initialHosts); const rdsHostListProvider = getRdsHostListProvider("someUrl"); const spiedProvider = spy(rdsHostListProvider); @@ -174,7 +170,9 @@ describe("testRdsHostListProvider", () => { const result = await rdsHostListProvider.getTopology(mockClientWrapper, true); expect(result.hosts).toBeTruthy(); - expect(result.hosts).toEqual(initialHosts); + for (let i = 0; i < result.hosts.length; i++) { + expect(result.hosts[i].equals(initialHosts[i])).toBeTruthy(); + } verify(spiedProvider.queryForTopology(anything(), anything())).atMost(1); }); @@ -270,13 +268,6 @@ describe("testRdsHostListProvider", () => { when(mockPluginService.isClientValid(anything())).thenResolve(true); - when(mockConnectionUrlParser.getHostsFromConnectionUrl(anyString(), anything(), anything(), anyFunction())).thenReturn([ - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "cluster-a.cluster-xyz.us-east-2.rds.amazonaws.com" - }) - ]); - const topologyClusterA: HostInfo[] = [ createHost({ hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), @@ -319,13 +310,6 @@ describe("testRdsHostListProvider", () => { when(mockPluginService.isClientValid(anything())).thenResolve(true); - when(mockConnectionUrlParser.getHostsFromConnectionUrl(anyString(), anything(), anything(), anyFunction())).thenReturn([ - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "cluster-a.cluster-xyz.us-east-2.rds.amazonaws.com" - }) - ]); - const topologyClusterA: HostInfo[] = [ createHost({ hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), @@ -353,7 +337,7 @@ describe("testRdsHostListProvider", () => { const topologyProvider1: HostInfo[] = await provider1.refresh(mockClientWrapper); expect(topologyProvider1).toEqual(topologyClusterA); - const provider2 = getRdsHostListProvider("instance-a-3.xyz.us-east-2.rds.amazonaws.com/"); + const provider2 = getRdsHostListProvider("instance-a-3.xyz.us-east-2.rds.amazonaws.com"); expect(provider2.clusterId).toEqual(provider1.clusterId); expect(provider1.isPrimaryClusterId).toBeTruthy(); @@ -368,13 +352,6 @@ describe("testRdsHostListProvider", () => { when(mockPluginService.isClientValid(anything())).thenResolve(true); - when(mockConnectionUrlParser.getHostsFromConnectionUrl(anyString(), anything(), anything(), anyFunction())).thenReturn([ - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-2.xyz.us-east-2.rds.amazonaws.com/" - }) - ]); - const topologyClusterA: HostInfo[] = [ createHost({ hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), @@ -393,7 +370,7 @@ describe("testRdsHostListProvider", () => { }) ]; - const provider1 = getRdsHostListProvider("instance-a-2.xyz.us-east-2.rds.amazonaws.com/"); + const provider1 = getRdsHostListProvider("instance-a-2.xyz.us-east-2.rds.amazonaws.com"); const spiedProvider1 = spy(provider1); when(spiedProvider1.queryForTopology(anything(), anything())).thenReturn(Promise.resolve(topologyClusterA)); @@ -402,7 +379,7 @@ describe("testRdsHostListProvider", () => { const topologyProvider1: HostInfo[] = await provider1.refresh(mockClientWrapper); expect(topologyProvider1).toEqual(topologyClusterA); - const provider2 = getRdsHostListProvider("cluster-a.cluster-xyz.us-east-2.rds.amazonaws.com/"); + const provider2 = getRdsHostListProvider("cluster-a.cluster-xyz.us-east-2.rds.amazonaws.com"); const spiedProvider2 = spy(provider2); when(spiedProvider2.queryForTopology(anything(), anything())).thenReturn(Promise.resolve(topologyClusterA)); @@ -412,7 +389,7 @@ describe("testRdsHostListProvider", () => { expect(await provider2.refresh(instance(mockClientWrapper))).toEqual(topologyClusterA); expect(RdsHostListProvider.topologyCache.size()).toEqual(2); - expect(RdsHostListProvider.suggestedPrimaryClusterIdCache.get(provider1.clusterId)).toEqual("cluster-a.cluster-xyz.us-east-2.rds.amazonaws.com/"); + expect(RdsHostListProvider.suggestedPrimaryClusterIdCache.get(provider1.clusterId)).toEqual("cluster-a.cluster-xyz.us-east-2.rds.amazonaws.com"); expect(await provider1.forceRefresh(instance(mockClientWrapper))).toEqual(topologyClusterA); expect(provider2.clusterId).toEqual(provider1.clusterId); @@ -422,7 +399,7 @@ describe("testRdsHostListProvider", () => { }); it("testIdentifyConnectionWithInvalidHostIdQuery", async () => { - when(mockDialect.identifyConnection(anything(), anything())).thenThrow(new AwsWrapperError("bad things")); + when(mockDialect.identifyConnection(anything())).thenThrow(new AwsWrapperError("bad things")); const rdsHostListProvider = getRdsHostListProvider("foo"); await expect(rdsHostListProvider.identifyConnection(instance(mockClientWrapper), instance(mockDialect))).rejects.toThrow(AwsWrapperError); diff --git a/tests/unit/rds_utils.test.ts b/tests/unit/rds_utils.test.ts index 71aaeff8..48773d57 100644 --- a/tests/unit/rds_utils.test.ts +++ b/tests/unit/rds_utils.test.ts @@ -21,28 +21,74 @@ const us_east_region_cluster_read_only = "database-test-name.cluster-ro-XYZ.us-e const us_east_region_instance = "instance-test-name.XYZ.us-east-2.rds.amazonaws.com"; const us_east_region_proxy = "proxy-test-name.proxy-XYZ.us-east-2.rds.amazonaws.com"; const us_east_region_custom_domain = "custom-test-name.cluster-custom-XYZ.us-east-2.rds.amazonaws.com"; +const usEastRegionElbUrl = "elb-name.elb.us-east-2.amazonaws.com"; + const china_region_cluster = "database-test-name.cluster-XYZ.rds.cn-northwest-1.amazonaws.com.cn"; +const old_china_region_cluster = "database-test-name.cluster-XYZ.cn-northwest-1.rds.amazonaws.com.cn"; const china_region_cluster_read_only = "database-test-name.cluster-ro-XYZ.rds.cn-northwest-1.amazonaws.com.cn"; +const old_china_region_cluster_read_only = "database-test-name.cluster-ro-XYZ.cn-northwest-1.rds.amazonaws.com.cn"; const china_region_instance = "instance-test-name.XYZ.rds.cn-northwest-1.amazonaws.com.cn"; +const old_china_region_instance = "instance-test-name.XYZ.cn-northwest-1.rds.amazonaws.com.cn"; const china_region_proxy = "proxy-test-name.proxy-XYZ.rds.cn-northwest-1.amazonaws.com.cn"; +const old_china_region_proxy = "proxy-test-name.proxy-XYZ.cn-northwest-1.rds.amazonaws.com.cn"; const china_region_custom_domain = "custom-test-name.cluster-custom-XYZ.rds.cn-northwest-1.amazonaws.com.cn"; +const old_china_region_custom_domain = "custom-test-name.cluster-custom-XYZ.cn-northwest-1.rds.amazonaws.com.cn"; + +const usIsobEastRegionCluster = "database-test-name.cluster-XYZ.rds.us-isob-east-1.sc2s.sgov.gov"; +const usIsobEastRegionClusterReadOnly = "database-test-name.cluster-ro-XYZ.rds.us-isob-east-1.sc2s.sgov.gov"; +const usIsobEastRegionInstance = "instance-test-name.XYZ.rds.us-isob-east-1.sc2s.sgov.gov"; +const usIsobEastRegionProxy = "proxy-test-name.proxy-XYZ.rds.us-isob-east-1.sc2s.sgov.gov"; +const usIsobEastRegionCustomDomain = "custom-test-name.cluster-custom-XYZ.rds.us-isob-east-1.sc2s.sgov.gov"; + +const usGovEastRegionCluster = "database-test-name.cluster-XYZ.rds.us-gov-east-1.amazonaws.com"; +const usIsoEastRegionCluster = "database-test-name.cluster-XYZ.rds.us-iso-east-1.c2s.ic.gov"; +const usIsoEastRegionClusterReadOnly = "database-test-name.cluster-ro-XYZ.rds.us-iso-east-1.c2s.ic.gov"; +const usIsoEastRegionInstance = "instance-test-name.XYZ.rds.us-iso-east-1.c2s.ic.gov"; +const usIsoEastRegionProxy = "proxy-test-name.proxy-XYZ.rds.us-iso-east-1.c2s.ic.gov"; +const usIsoEastRegionCustomDomain = "custom-test-name.cluster-custom-XYZ.rds.us-iso-east-1.c2s.ic.gov"; + +const extraRdsChinaPath = "database-test-name.cluster-XYZ.rds.cn-northwest-1.rds.amazonaws.com.cn"; +const missingCnChinaPath = "database-test-name.cluster-XYZ.rds.cn-northwest-1.amazonaws.com"; +const missingRegionChinaPath = "database-test-name.cluster-XYZ.rds.amazonaws.com.cn"; describe("test_rds_utils", () => { - it.each([[us_east_region_cluster], [us_east_region_cluster_read_only], [china_region_cluster], [china_region_cluster_read_only]])( - "test_is_rds_cluster_dns", - (val) => { - const target = new RdsUtils(); - expect(target.isRdsClusterDns(val)).toBeTruthy(); - } - ); + beforeEach(() => { + RdsUtils.clearCache(); + }); + + it.each([ + [us_east_region_cluster], + [us_east_region_cluster_read_only], + [china_region_cluster], + [china_region_cluster_read_only], + [old_china_region_cluster], + [old_china_region_cluster_read_only], + [usIsobEastRegionCluster], + [usIsobEastRegionClusterReadOnly], + [usIsoEastRegionCluster], + [usIsoEastRegionClusterReadOnly] + ])("test_is_rds_cluster_dns", (val) => { + const target = new RdsUtils(); + expect(target.isRdsClusterDns(val)).toBeTruthy(); + }); it.each([ [us_east_region_instance], [us_east_region_proxy], [us_east_region_custom_domain], + [usEastRegionElbUrl], [china_region_instance], [china_region_proxy], - [china_region_custom_domain] + [china_region_custom_domain], + [old_china_region_instance], + [old_china_region_proxy], + [old_china_region_custom_domain], + [usIsobEastRegionInstance], + [usIsobEastRegionProxy], + [usIsobEastRegionCustomDomain], + [usIsoEastRegionInstance], + [usIsoEastRegionProxy], + [usIsoEastRegionCustomDomain] ])("test_is_not_rds_cluster_dns", (val) => { const target = new RdsUtils(); expect(target.isRdsClusterDns(val)).toBeFalsy(); @@ -58,12 +104,32 @@ describe("test_rds_utils", () => { [china_region_cluster_read_only], [china_region_instance], [china_region_proxy], - [china_region_custom_domain] + [china_region_custom_domain], + [old_china_region_cluster], + [old_china_region_cluster_read_only], + [old_china_region_instance], + [old_china_region_proxy], + [old_china_region_custom_domain], + [usIsobEastRegionCluster], + [usIsobEastRegionClusterReadOnly], + [usIsobEastRegionInstance], + [usIsobEastRegionProxy], + [usIsobEastRegionCustomDomain], + [usIsoEastRegionCluster], + [usIsoEastRegionClusterReadOnly], + [usIsoEastRegionInstance], + [usIsoEastRegionProxy], + [usIsoEastRegionCustomDomain] ])("test_is_rds_dns", (val) => { const target = new RdsUtils(); expect(target.isRdsDns(val)).toBeTruthy(); }); + it.each([[usEastRegionElbUrl]])("test_is_not_rds_dns", (val) => { + const target = new RdsUtils(); + expect(target.isRdsDns(val)).toBeFalsy(); + }); + it.each([ ["?.XYZ.us-east-2.rds.amazonaws.com", us_east_region_cluster], ["?.XYZ.us-east-2.rds.amazonaws.com", us_east_region_cluster_read_only], @@ -74,7 +140,23 @@ describe("test_rds_utils", () => { ["?.XYZ.rds.cn-northwest-1.amazonaws.com.cn", china_region_cluster_read_only], ["?.XYZ.rds.cn-northwest-1.amazonaws.com.cn", china_region_instance], ["?.XYZ.rds.cn-northwest-1.amazonaws.com.cn", china_region_proxy], - ["?.XYZ.rds.cn-northwest-1.amazonaws.com.cn", china_region_custom_domain] + ["?.XYZ.rds.cn-northwest-1.amazonaws.com.cn", china_region_custom_domain], + ["?.XYZ.cn-northwest-1.rds.amazonaws.com.cn", old_china_region_cluster], + ["?.XYZ.cn-northwest-1.rds.amazonaws.com.cn", old_china_region_cluster_read_only], + ["?.XYZ.cn-northwest-1.rds.amazonaws.com.cn", old_china_region_instance], + ["?.XYZ.cn-northwest-1.rds.amazonaws.com.cn", old_china_region_proxy], + ["?.XYZ.cn-northwest-1.rds.amazonaws.com.cn", old_china_region_custom_domain], + ["?.XYZ.rds.us-gov-east-1.amazonaws.com", usGovEastRegionCluster], + ["?.XYZ.rds.us-isob-east-1.sc2s.sgov.gov", usIsobEastRegionCluster], + ["?.XYZ.rds.us-isob-east-1.sc2s.sgov.gov", usIsobEastRegionClusterReadOnly], + ["?.XYZ.rds.us-isob-east-1.sc2s.sgov.gov", usIsobEastRegionInstance], + ["?.XYZ.rds.us-isob-east-1.sc2s.sgov.gov", usIsobEastRegionProxy], + ["?.XYZ.rds.us-isob-east-1.sc2s.sgov.gov", usIsobEastRegionCustomDomain], + ["?.XYZ.rds.us-iso-east-1.c2s.ic.gov", usIsoEastRegionCluster], + ["?.XYZ.rds.us-iso-east-1.c2s.ic.gov", usIsoEastRegionClusterReadOnly], + ["?.XYZ.rds.us-iso-east-1.c2s.ic.gov", usIsoEastRegionInstance], + ["?.XYZ.rds.us-iso-east-1.c2s.ic.gov", usIsoEastRegionProxy], + ["?.XYZ.rds.us-iso-east-1.c2s.ic.gov", usIsoEastRegionCustomDomain] ])("test_get_rds_instance_host_pattern", (expected: string, val) => { const target = new RdsUtils(); expect(target.getRdsInstanceHostPattern(val)).toEqual(expected); @@ -90,16 +172,35 @@ describe("test_rds_utils", () => { ["cn-northwest-1", china_region_cluster_read_only], ["cn-northwest-1", china_region_instance], ["cn-northwest-1", china_region_proxy], - ["cn-northwest-1", china_region_custom_domain] + ["cn-northwest-1", china_region_custom_domain], + ["cn-northwest-1", old_china_region_cluster], + ["cn-northwest-1", old_china_region_cluster_read_only], + ["cn-northwest-1", old_china_region_instance], + ["cn-northwest-1", old_china_region_proxy], + ["cn-northwest-1", old_china_region_custom_domain], + ["us-gov-east-1", usGovEastRegionCluster], + ["us-isob-east-1", usIsobEastRegionCluster], + ["us-isob-east-1", usIsobEastRegionClusterReadOnly], + ["us-isob-east-1", usIsobEastRegionInstance], + ["us-isob-east-1", usIsobEastRegionProxy], + ["us-isob-east-1", usIsobEastRegionCustomDomain], + ["us-iso-east-1", usIsoEastRegionCluster], + ["us-iso-east-1", usIsoEastRegionClusterReadOnly], + ["us-iso-east-1", usIsoEastRegionInstance], + ["us-iso-east-1", usIsoEastRegionProxy], + ["us-iso-east-1", usIsoEastRegionCustomDomain] ])("test_get_rds_region", (expected: string, val) => { const target = new RdsUtils(); expect(target.getRdsRegion(val)).toEqual(expected); }); - it.each([[us_east_region_cluster], [china_region_cluster]])("test_is_writer_cluster_dns", (val) => { - const target = new RdsUtils(); - expect(target.isWriterClusterDns(val)).toBeTruthy(); - }); + it.each([[us_east_region_cluster], [china_region_cluster], [old_china_region_cluster], [usIsobEastRegionCluster], [usIsoEastRegionCluster]])( + "test_is_writer_cluster_dns", + (val) => { + const target = new RdsUtils(); + expect(target.isWriterClusterDns(val)).toBeTruthy(); + } + ); it.each([ [us_east_region_cluster_read_only], @@ -109,13 +210,31 @@ describe("test_rds_utils", () => { [china_region_cluster_read_only], [china_region_instance], [china_region_proxy], - [china_region_custom_domain] + [china_region_custom_domain], + [old_china_region_cluster_read_only], + [old_china_region_instance], + [old_china_region_proxy], + [old_china_region_custom_domain], + [usIsobEastRegionClusterReadOnly], + [usIsobEastRegionInstance], + [usIsobEastRegionProxy], + [usIsobEastRegionCustomDomain], + [usIsoEastRegionClusterReadOnly], + [usIsoEastRegionInstance], + [usIsoEastRegionProxy], + [usIsoEastRegionCustomDomain] ])("test_is_not_writer_cluster_dns", (val) => { const target = new RdsUtils(); expect(target.isWriterClusterDns(val)).toBeFalsy(); }); - it.each([[us_east_region_cluster_read_only], [china_region_cluster_read_only]])("test_is_reader_cluster_dns", (val) => { + it.each([ + [us_east_region_cluster_read_only], + [china_region_cluster_read_only], + [old_china_region_cluster_read_only], + [usIsobEastRegionClusterReadOnly], + [usIsoEastRegionClusterReadOnly] + ])("test_is_reader_cluster_dns", (val) => { const target = new RdsUtils(); expect(target.isReaderClusterDns(val)).toBeTruthy(); }); @@ -128,7 +247,19 @@ describe("test_rds_utils", () => { [china_region_cluster], [china_region_instance], [china_region_proxy], - [china_region_custom_domain] + [china_region_custom_domain], + [old_china_region_cluster], + [old_china_region_instance], + [old_china_region_proxy], + [old_china_region_custom_domain], + [usIsobEastRegionCluster], + [usIsobEastRegionInstance], + [usIsobEastRegionProxy], + [usIsobEastRegionCustomDomain], + [usIsoEastRegionCluster], + [usIsoEastRegionInstance], + [usIsoEastRegionProxy], + [usIsoEastRegionCustomDomain] ])("test_is_not_reader_cluster_dns", (val) => { const target = new RdsUtils(); expect(target.isReaderClusterDns(val)).toBeFalsy(); @@ -136,13 +267,16 @@ describe("test_rds_utils", () => { it("test_get_rds_cluster_host_url", () => { const expected: string = "foo.cluster-xyz.us-west-1.rds.amazonaws.com"; - const expected2: string = "foo-1.cluster-xyz.us-west-1.rds.amazonaws.com.cn"; + const expected2: string = "foo-2.cluster-xyz.rds.us-west-1.amazonaws.com.cn"; + const expected3: string = "foo-3.cluster-xyz.us-west-1.rds.amazonaws.com.cn"; const ro_endpoint: string = "foo.cluster-ro-xyz.us-west-1.rds.amazonaws.com"; - const china_ro_endpoint: string = "foo-1.cluster-ro-xyz.us-west-1.rds.amazonaws.com.cn"; + const china_ro_endpoint: string = "foo-2.cluster-ro-xyz.rds.us-west-1.amazonaws.com.cn"; + const old_china_ro_endpoint: string = "foo-3.cluster-ro-xyz.us-west-1.rds.amazonaws.com.cn"; const target = new RdsUtils(); expect(target.getRdsClusterHostUrl(ro_endpoint)).toEqual(expected); expect(target.getRdsClusterHostUrl(china_ro_endpoint)).toEqual(expected2); + expect(target.getRdsClusterHostUrl(old_china_ro_endpoint)).toEqual(expected3); }); it("test_green_instance_host_name", () => { @@ -182,4 +316,40 @@ describe("test_rds_utils", () => { ); expect(target.removeGreenInstancePrefix("test-instance-green-123456-green-123456.domain.com")).toBe("test-instance-green-123456.domain.com"); }); + + it("test_broken_paths_host_pattern", () => { + const target = new RdsUtils(); + const incorrectChinaHostPattern = "?.rds.cn-northwest-1.rds.amazonaws.com.cn"; + expect(target.getRdsInstanceHostPattern(extraRdsChinaPath)).toBe(incorrectChinaHostPattern); + expect(target.getRdsInstanceHostPattern(missingRegionChinaPath)).toBe("?"); + }); + + it("test_broken_paths_region", () => { + // Extra rds path returns correct region. + const target = new RdsUtils(); + const chinaExpectedRegion = "cn-northwest-1"; + expect(target.getRdsRegion(extraRdsChinaPath)).toBe(chinaExpectedRegion); + expect(target.getRdsRegion(missingRegionChinaPath)).toBeNull(); + }); + + it("test_broken_paths_reader_cluster", () => { + const target = new RdsUtils(); + expect(target.isReaderClusterDns(extraRdsChinaPath)).toBeFalsy(); + expect(target.isReaderClusterDns(missingCnChinaPath)).toBeFalsy(); + expect(target.isReaderClusterDns(missingRegionChinaPath)).toBeFalsy(); + }); + + it("test_broken_paths_writer_cluster", () => { + // Expected to return true with correct cluster paths. + const target = new RdsUtils(); + expect(target.isWriterClusterDns(extraRdsChinaPath)).toBeFalsy(); + expect(target.isWriterClusterDns(missingRegionChinaPath)).toBeFalsy(); + }); + + it("test_broken_paths_rds_dns", () => { + // Expected to return true with correct cluster paths. + const target = new RdsUtils(); + expect(target.isRdsDns(extraRdsChinaPath)).toBeTruthy(); + expect(target.isRdsDns(missingRegionChinaPath)).toBeFalsy(); + }); });