diff --git a/common/lib/authentication/aws_credentials_manager.ts b/common/lib/authentication/aws_credentials_manager.ts index c7f2a9b2..09059953 100644 --- a/common/lib/authentication/aws_credentials_manager.ts +++ b/common/lib/authentication/aws_credentials_manager.ts @@ -17,29 +17,34 @@ import { HostInfo } from "../host_info"; import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; import { AwsCredentialIdentityProvider } from "@smithy/types/dist-types/identity/awsCredentialIdentity"; +import { WrapperProperties } from "../wrapper_property"; +import { AwsWrapperError } from "../utils/errors"; +import { Messages } from "../utils/messages"; interface AwsCredentialsProviderHandler { getAwsCredentialsProvider(hostInfo: HostInfo, properties: Map): AwsCredentialIdentityProvider; } export class AwsCredentialsManager { - private static handler?: AwsCredentialsProviderHandler; - - static setCustomHandler(customHandler: AwsCredentialsProviderHandler) { - AwsCredentialsManager.handler = customHandler; - } - static getProvider(hostInfo: HostInfo, props: Map): AwsCredentialIdentityProvider { - return AwsCredentialsManager.handler === undefined - ? AwsCredentialsManager.getDefaultProvider() - : AwsCredentialsManager.handler.getAwsCredentialsProvider(hostInfo, props); - } + const awsCredentialProviderHandler = WrapperProperties.CUSTOM_AWS_CREDENTIAL_PROVIDER_HANDLER.get(props); + if (awsCredentialProviderHandler && !AwsCredentialsManager.isAwsCredentialsProviderHandler(awsCredentialProviderHandler)) { + throw new AwsWrapperError(Messages.get("AwsCredentialsManager.wrongHandler")); + } - static resetCustomHandler() { - AwsCredentialsManager.handler = undefined; + return !awsCredentialProviderHandler + ? AwsCredentialsManager.getDefaultProvider(WrapperProperties.AWS_PROFILE.get(props)) + : awsCredentialProviderHandler.getAwsCredentialsProvider(hostInfo, props); } - private static getDefaultProvider() { + private static getDefaultProvider(profileName: string | null) { + if (profileName) { + return fromNodeProviderChain({ profile: profileName }); + } return fromNodeProviderChain(); } + + private static isAwsCredentialsProviderHandler(arg: any): arg is AwsCredentialsProviderHandler { + return arg.getAwsCredentialsProvider !== undefined; + } } diff --git a/common/lib/aws_client.ts b/common/lib/aws_client.ts index ce124077..6fb9fe72 100644 --- a/common/lib/aws_client.ts +++ b/common/lib/aws_client.ts @@ -31,6 +31,10 @@ import { DefaultTelemetryFactory } from "./utils/telemetry/default_telemetry_fac import { TelemetryFactory } from "./utils/telemetry/telemetry_factory"; import { DriverDialect } from "./driver_dialect/driver_dialect"; import { WrapperProperties } from "./wrapper_property"; +import { DriverConfigurationProfiles } from "./profile/driver_configuration_profiles"; +import { ConfigurationProfile } from "./profile/configuration_profile"; +import { AwsWrapperError } from "./utils/errors"; +import { Messages } from "./utils/messages"; export abstract class AwsClient extends EventEmitter { private _defaultPort: number = -1; @@ -41,6 +45,7 @@ export abstract class AwsClient extends EventEmitter { protected _isReadOnly: boolean = false; protected _isolationLevel: number = 0; protected _connectionUrlParser: ConnectionUrlParser; + protected _configurationProfile: ConfigurationProfile | null = null; readonly properties: Map; config: any; targetClient?: ClientWrapper; @@ -58,9 +63,50 @@ export abstract class AwsClient extends EventEmitter { this.properties = new Map(Object.entries(config)); + const profileName = WrapperProperties.PROFILE_NAME.get(this.properties); + if (profileName && profileName.length > 0) { + this._configurationProfile = DriverConfigurationProfiles.getProfileConfiguration(profileName); + if (this._configurationProfile) { + const profileProperties = this._configurationProfile.getProperties(); + if (profileProperties) { + for (const key of profileProperties.keys()) { + if (this.properties.has(key)) { + // Setting defined by a user has priority over property in configuration profile. + continue; + } + this.properties.set(key, profileProperties.get(key)); + } + + const connectionProvider = WrapperProperties.CONNECTION_PROVIDER.get(this.properties); + if (!connectionProvider) { + WrapperProperties.CONNECTION_PROVIDER.set(this.properties, this._configurationProfile.getAwsCredentialProvider()); + } + + const customAwsCredentialProvider = WrapperProperties.CUSTOM_AWS_CREDENTIAL_PROVIDER_HANDLER.get(this.properties); + if (!customAwsCredentialProvider) { + WrapperProperties.CUSTOM_AWS_CREDENTIAL_PROVIDER_HANDLER.set(this.properties, this._configurationProfile.getAwsCredentialProvider()); + } + + const customDatabaseDialect = WrapperProperties.CUSTOM_DATABASE_DIALECT.get(this.properties); + if (!customDatabaseDialect) { + WrapperProperties.CUSTOM_DATABASE_DIALECT.set(this.properties, this._configurationProfile.getDatabaseDialect()); + } + } + } else { + throw new AwsWrapperError(Messages.get("AwsClient.configurationProfileNotFound", profileName)); + } + } + this.telemetryFactory = new DefaultTelemetryFactory(this.properties); const container = new PluginServiceManagerContainer(); - this.pluginService = new PluginService(container, this, dbType, knownDialectsByCode, this.properties, driverDialect); + this.pluginService = new PluginService( + container, + this, + dbType, + knownDialectsByCode, + this.properties, + this._configurationProfile?.getDriverDialect() ?? driverDialect + ); this.pluginManager = new PluginManager( container, this.properties, @@ -71,7 +117,7 @@ export abstract class AwsClient extends EventEmitter { private async setup() { await this.telemetryFactory.init(); - await this.pluginManager.init(); + await this.pluginManager.init(this._configurationProfile); } protected async internalConnect() { diff --git a/common/lib/connection_plugin_chain_builder.ts b/common/lib/connection_plugin_chain_builder.ts index 745ed224..7a8817c6 100644 --- a/common/lib/connection_plugin_chain_builder.ts +++ b/common/lib/connection_plugin_chain_builder.ts @@ -38,6 +38,7 @@ import { DeveloperConnectionPluginFactory } from "./plugins/dev/developer_connec import { ConnectionPluginFactory } from "./plugin_factory"; import { LimitlessConnectionPluginFactory } from "./plugins/limitless/limitless_connection_plugin_factory"; import { FastestResponseStrategyPluginFactory } from "./plugins/strategy/fastest_response/fastest_respose_strategy_plugin_factory"; +import { ConfigurationProfile } from "./profile/configuration_profile"; /* Type alias used for plugin factory sorting. It holds a reference to a plugin @@ -69,58 +70,90 @@ export class ConnectionPluginChainBuilder { ["executeTime", { factory: ExecuteTimePluginFactory, weight: ConnectionPluginChainBuilder.WEIGHT_RELATIVE_TO_PRIOR_PLUGIN }] ]); + static readonly PLUGIN_WEIGHTS = new Map([ + [AuroraInitialConnectionStrategyFactory, 390], + [AuroraConnectionTrackerPluginFactory, 400], + [StaleDnsPluginFactory, 500], + [ReadWriteSplittingPluginFactory, 600], + [FailoverPluginFactory, 700], + [HostMonitoringPluginFactory, 800], + [LimitlessConnectionPluginFactory, 950], + [IamAuthenticationPluginFactory, 1000], + [AwsSecretsManagerPluginFactory, 1100], + [FederatedAuthPluginFactory, 1200], + [OktaAuthPluginFactory, 1300], + [DeveloperConnectionPluginFactory, 1400], + [ConnectTimePluginFactory, ConnectionPluginChainBuilder.WEIGHT_RELATIVE_TO_PRIOR_PLUGIN], + [ExecuteTimePluginFactory, ConnectionPluginChainBuilder.WEIGHT_RELATIVE_TO_PRIOR_PLUGIN] + ]); + static async getPlugins( pluginService: PluginService, props: Map, - connectionProviderManager: ConnectionProviderManager + connectionProviderManager: ConnectionProviderManager, + configurationProfile: ConfigurationProfile | null ): Promise { + let pluginFactoryInfoList: PluginFactoryInfo[] = []; const plugins: ConnectionPlugin[] = []; - let pluginCodes: string = props.get(WrapperProperties.PLUGINS.name); - if (pluginCodes == null) { - pluginCodes = WrapperProperties.DEFAULT_PLUGINS; - } - - const usingDefault = pluginCodes === WrapperProperties.DEFAULT_PLUGINS; + let usingDefault: boolean = false; - pluginCodes = pluginCodes.trim(); - - if (pluginCodes !== "") { - const pluginCodeList = pluginCodes.split(",").map((pluginCode) => pluginCode.trim()); - let pluginFactoryInfoList: PluginFactoryInfo[] = []; - let lastWeight = 0; - pluginCodeList.forEach((p) => { - if (!ConnectionPluginChainBuilder.PLUGIN_FACTORIES.has(p)) { - throw new AwsWrapperError(Messages.get("PluginManager.unknownPluginCode", p)); - } - - const factoryInfo = ConnectionPluginChainBuilder.PLUGIN_FACTORIES.get(p); - if (factoryInfo) { - if (factoryInfo.weight === ConnectionPluginChainBuilder.WEIGHT_RELATIVE_TO_PRIOR_PLUGIN) { - lastWeight++; - } else { - lastWeight = factoryInfo.weight; + if (configurationProfile) { + const profilePluginFactories = configurationProfile.getPluginFactories(); + if (profilePluginFactories) { + for (const factory of profilePluginFactories) { + const weight = ConnectionPluginChainBuilder.PLUGIN_WEIGHTS.get(factory); + if (!weight) { + throw new AwsWrapperError(Messages.get("PluginManager.unknownPluginWeight", factory.prototype.constructor.name)); } - pluginFactoryInfoList.push({ factory: factoryInfo.factory, weight: lastWeight }); + pluginFactoryInfoList.push({ factory: factory, weight: weight }); } - }); + usingDefault = true; // We assume that plugin factories in configuration profile is presorted. + } + } else { + let pluginCodes: string = props.get(WrapperProperties.PLUGINS.name); + if (pluginCodes == null) { + pluginCodes = WrapperProperties.DEFAULT_PLUGINS; + } + usingDefault = pluginCodes === WrapperProperties.DEFAULT_PLUGINS; - if (!usingDefault && pluginFactoryInfoList.length > 1 && WrapperProperties.AUTO_SORT_PLUGIN_ORDER.get(props)) { - pluginFactoryInfoList = pluginFactoryInfoList.sort((a, b) => a.weight - b.weight); + pluginCodes = pluginCodes.trim(); + if (pluginCodes !== "") { + const pluginCodeList = pluginCodes.split(",").map((pluginCode) => pluginCode.trim()); + let lastWeight = 0; + pluginCodeList.forEach((p) => { + if (!ConnectionPluginChainBuilder.PLUGIN_FACTORIES.has(p)) { + throw new AwsWrapperError(Messages.get("PluginManager.unknownPluginCode", p)); + } - if (!usingDefault) { - logger.info( - "Plugins order has been rearranged. The following order is in effect: " + - pluginFactoryInfoList.map((pluginFactoryInfo) => pluginFactoryInfo.factory.name.split("Factory")[0]).join(", ") - ); - } + const factoryInfo = ConnectionPluginChainBuilder.PLUGIN_FACTORIES.get(p); + if (factoryInfo) { + if (factoryInfo.weight === ConnectionPluginChainBuilder.WEIGHT_RELATIVE_TO_PRIOR_PLUGIN) { + lastWeight++; + } else { + lastWeight = factoryInfo.weight; + } + pluginFactoryInfoList.push({ factory: factoryInfo.factory, weight: lastWeight }); + } + }); } + } + + if (!usingDefault && pluginFactoryInfoList.length > 1 && WrapperProperties.AUTO_SORT_PLUGIN_ORDER.get(props)) { + pluginFactoryInfoList = pluginFactoryInfoList.sort((a, b) => a.weight - b.weight); - for (const pluginFactoryInfo of pluginFactoryInfoList) { - const factoryObj = new pluginFactoryInfo.factory(); - plugins.push(await factoryObj.getInstance(pluginService, props)); + if (!usingDefault) { + logger.info( + "Plugins order has been rearranged. The following order is in effect: " + + pluginFactoryInfoList.map((pluginFactoryInfo) => pluginFactoryInfo.factory.name.split("Factory")[0]).join(", ") + ); } } + for (const pluginFactoryInfo of pluginFactoryInfoList) { + const factoryObj = new pluginFactoryInfo.factory(); + plugins.push(await factoryObj.getInstance(pluginService, props)); + } + plugins.push(new DefaultPlugin(pluginService, connectionProviderManager)); return plugins; diff --git a/common/lib/database_dialect/database_dialect_manager.ts b/common/lib/database_dialect/database_dialect_manager.ts index f8bcf26a..e8127f2a 100644 --- a/common/lib/database_dialect/database_dialect_manager.ts +++ b/common/lib/database_dialect/database_dialect_manager.ts @@ -34,33 +34,36 @@ export class DatabaseDialectManager implements DatabaseDialectProvider { */ private static readonly ENDPOINT_CACHE_EXPIRATION_MS = 86_400_000_000_000; // 24 hours protected static readonly knownEndpointDialects: CacheMap = new CacheMap(); - protected readonly knownDialectsByCode: Map; - private static customDialect: DatabaseDialect | null = null; - private readonly rdsHelper: RdsUtils = new RdsUtils(); - private readonly dbType; - private canUpdate: boolean = false; - private dialect: DatabaseDialect; - private dialectCode: string = ""; + protected readonly knownDialectsByCode: Map; + protected readonly customDialect: DatabaseDialect | null; + protected readonly rdsHelper: RdsUtils = new RdsUtils(); + protected readonly dbType: DatabaseType; + protected canUpdate: boolean = false; + protected dialect: DatabaseDialect; + protected dialectCode: string = ""; constructor(knownDialectsByCode: any, dbType: DatabaseType, props: Map) { this.knownDialectsByCode = knownDialectsByCode; this.dbType = dbType; - this.dialect = this.getDialect(props); - } - static setCustomDialect(dialect: DatabaseDialect) { - DatabaseDialectManager.customDialect = dialect; - } + const dialectSetting = WrapperProperties.CUSTOM_DATABASE_DIALECT.get(props); + if (dialectSetting && !this.isDatabaseDialect(dialectSetting)) { + throw new AwsWrapperError(Messages.get("DatabaseDialectManager.wrongCustomDialect")); + } + this.customDialect = dialectSetting; - static resetCustomDialect() { - DatabaseDialectManager.customDialect = null; + this.dialect = this.getDialect(props); } static resetEndpointCache() { DatabaseDialectManager.knownEndpointDialects.clear(); } + protected isDatabaseDialect(arg: any): arg is DatabaseDialect { + return arg.getDialectName !== undefined; + } + getDialect(props: Map): DatabaseDialect { if (this.dialect) { return this.dialect; @@ -68,9 +71,9 @@ export class DatabaseDialectManager implements DatabaseDialectProvider { this.canUpdate = false; - if (DatabaseDialectManager.customDialect) { + if (this.customDialect) { this.dialectCode = DatabaseDialectCodes.CUSTOM; - this.dialect = DatabaseDialectManager.customDialect; + this.dialect = this.customDialect; this.logCurrentDialect(); return this.dialect; } @@ -87,7 +90,7 @@ export class DatabaseDialectManager implements DatabaseDialectProvider { this.logCurrentDialect(); return userDialect; } - throw new AwsWrapperError(Messages.get("DialectManager.unknownDialectCode", dialectCode)); + throw new AwsWrapperError(Messages.get("DatabaseDialectManager.unknownDialectCode", dialectCode)); } if (this.dbType === DatabaseType.MYSQL) { @@ -148,7 +151,7 @@ export class DatabaseDialectManager implements DatabaseDialectProvider { return this.dialect; } - throw new AwsWrapperError(Messages.get("DialectManager.getDialectError")); + throw new AwsWrapperError(Messages.get("DatabaseDialectManager.getDialectError")); } async getDialectForUpdate(targetClient: ClientWrapper, originalHost: string, newHost: string): Promise { @@ -161,7 +164,7 @@ export class DatabaseDialectManager implements DatabaseDialectProvider { for (const dialectCandidateCode of dialectCandidates) { const dialectCandidate = this.knownDialectsByCode.get(dialectCandidateCode); if (!dialectCandidate) { - throw new AwsWrapperError(Messages.get("DialectManager.unknownDialectCode", dialectCandidateCode)); + throw new AwsWrapperError(Messages.get("DatabaseDialectManager.unknownDialectCode", dialectCandidateCode)); } const isDialect = await dialectCandidate.isDialect(targetClient); diff --git a/common/lib/plugin_manager.ts b/common/lib/plugin_manager.ts index 51317cb7..9409dad3 100644 --- a/common/lib/plugin_manager.ts +++ b/common/lib/plugin_manager.ts @@ -31,6 +31,7 @@ import { TelemetryFactory } from "./utils/telemetry/telemetry_factory"; import { TelemetryTraceLevel } from "./utils/telemetry/telemetry_trace_level"; import { ConnectionProvider } from "./connection_provider"; import { ConnectionPluginFactory } from "./plugin_factory"; +import { ConfigurationProfile } from "./profile/configuration_profile"; type PluginFunc = (plugin: ConnectionPlugin, targetFunc: () => Promise) => Promise; @@ -91,9 +92,9 @@ export class PluginManager { this.telemetryFactory = telemetryFactory; } - async init(): Promise; - async init(plugins: ConnectionPlugin[]): Promise; - async init(plugins?: ConnectionPlugin[]) { + async init(configurationProfile?: ConfigurationProfile | null): Promise; + async init(configurationProfile: ConfigurationProfile | null, plugins: ConnectionPlugin[]): Promise; + async init(configurationProfile: ConfigurationProfile | null, plugins?: ConnectionPlugin[]) { if (this.pluginServiceManagerContainer.pluginService != null) { if (plugins) { this._plugins = plugins; @@ -101,7 +102,8 @@ export class PluginManager { this._plugins = await ConnectionPluginChainBuilder.getPlugins( this.pluginServiceManagerContainer.pluginService, this.props, - this.connectionProviderManager + this.connectionProviderManager, + configurationProfile ); } } diff --git a/common/lib/plugin_service.ts b/common/lib/plugin_service.ts index 32716414..8284c1f1 100644 --- a/common/lib/plugin_service.ts +++ b/common/lib/plugin_service.ts @@ -43,6 +43,7 @@ import { DatabaseDialectCodes } from "./database_dialect/database_dialect_codes" import { getWriter } from "./utils/utils"; import { TelemetryFactory } from "./utils/telemetry/telemetry_factory"; import { DriverDialect } from "./driver_dialect/driver_dialect"; +import { ConfigurationProfile } from "./profile/configuration_profile"; export class PluginService implements ErrorHandler, HostListProviderService { private readonly _currentClient: AwsClient; @@ -77,7 +78,7 @@ export class PluginService implements ErrorHandler, HostListProviderService { this.sessionStateService = new SessionStateServiceImpl(this, this.props); container.pluginService = this; - this.dialect = this.dbDialectProvider.getDialect(this.props); + this.dialect = WrapperProperties.CUSTOM_DATABASE_DIALECT.get(this.props) ?? this.dbDialectProvider.getDialect(this.props); } isInTransaction(): boolean { diff --git a/common/lib/profile/configuration_profile.ts b/common/lib/profile/configuration_profile.ts new file mode 100644 index 00000000..9a52d355 --- /dev/null +++ b/common/lib/profile/configuration_profile.ts @@ -0,0 +1,130 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ConnectionPluginFactory } from "../plugin_factory"; +import { DatabaseDialect } from "../database_dialect/database_dialect"; +import { DriverDialect } from "../driver_dialect/driver_dialect"; +import { ConnectionProvider } from "../connection_provider"; + +export class ConfigurationProfile { + protected readonly name: string; + protected readonly pluginFactories: (typeof ConnectionPluginFactory)[]; + protected readonly properties: Map; + protected readonly databaseDialect: DatabaseDialect | (() => DatabaseDialect) | null; + protected readonly driverDialect: DriverDialect | (() => DriverDialect) | null; + protected readonly awsCredentialProvider: any | null; //AwsCredentialsProviderHandler + protected readonly connectionProvider: ConnectionProvider | (() => ConnectionProvider) | null; + + // Initialized objects + protected databaseDialectObj: DatabaseDialect | null = null; + protected driverDialectObj: DriverDialect | null = null; + protected awsCredentialProviderObj: any | null = null; //AwsCredentialsProviderHandler + protected connectionProviderObj: ConnectionProvider | null = null; + + constructor( + name: string, + pluginFactories: (typeof ConnectionPluginFactory)[], // Factories should be presorted by weights! + properties: Map, + databaseDialect: DatabaseDialect | (() => DatabaseDialect) | null, + driverDialect: DriverDialect | (() => DriverDialect) | null, + awsCredentialProvider: any, + connectionProvider: ConnectionProvider | (() => ConnectionProvider) | null + ) { + this.name = name; + this.pluginFactories = pluginFactories; + this.properties = properties; + this.databaseDialect = databaseDialect; + this.driverDialect = driverDialect; + this.awsCredentialProvider = awsCredentialProvider; + this.connectionProvider = connectionProvider; + } + + public getName(): string { + return this.name; + } + + public getProperties(): Map { + return this.properties; + } + + public getPluginFactories(): (typeof ConnectionPluginFactory)[] { + return this.pluginFactories; + } + + public getDatabaseDialect(): DatabaseDialect | null { + if (this.databaseDialectObj) { + return this.databaseDialectObj; + } + if (!this.databaseDialect) { + return null; + } + + if (typeof this.driverDialect === "function") { + this.databaseDialectObj = (this.databaseDialect as () => DatabaseDialect)(); + } else { + this.databaseDialectObj = this.databaseDialect as DatabaseDialect; + } + return this.databaseDialectObj; + } + + public getDriverDialect(): DriverDialect | null { + if (this.driverDialectObj) { + return this.driverDialectObj; + } + if (!this.driverDialect) { + return null; + } + + if (typeof this.driverDialect === "function") { + this.driverDialectObj = (this.driverDialect as () => DriverDialect)(); + } else { + this.driverDialectObj = this.driverDialect as DriverDialect; + } + return this.driverDialectObj; + } + + public getConnectionProvider(): ConnectionProvider | null { + if (this.connectionProviderObj) { + return this.connectionProviderObj; + } + if (!this.connectionProvider) { + return null; + } + + if (typeof this.connectionProvider === "function") { + this.connectionProviderObj = (this.connectionProvider as () => ConnectionProvider)(); + } else { + this.connectionProviderObj = this.connectionProvider as ConnectionProvider; + } + return this.connectionProviderObj; + } + + public getAwsCredentialProvider(): any | null { + if (this.awsCredentialProviderObj) { + return this.awsCredentialProviderObj; + } + if (!this.awsCredentialProvider) { + return null; + } + + if (typeof this.awsCredentialProvider === "function") { + this.awsCredentialProviderObj = (this.awsCredentialProvider as () => any)(); + } else { + this.awsCredentialProviderObj = this.awsCredentialProvider; + } + return this.awsCredentialProviderObj; + } +} diff --git a/common/lib/profile/configuration_profile_builder.ts b/common/lib/profile/configuration_profile_builder.ts new file mode 100644 index 00000000..8868db2f --- /dev/null +++ b/common/lib/profile/configuration_profile_builder.ts @@ -0,0 +1,115 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ConnectionPluginFactory } from "../plugin_factory"; +import { DatabaseDialect } from "../database_dialect/database_dialect"; +import { DriverDialect } from "../driver_dialect/driver_dialect"; +import { ConnectionProvider } from "../connection_provider"; +import { AwsWrapperError } from "../utils/errors"; +import { Messages } from "../utils/messages"; +import { ConfigurationProfile } from "./configuration_profile"; +import { ConfigurationProfilePresetCodes } from "./configuration_profile_codes"; +import { DriverConfigurationProfiles } from "./driver_configuration_profiles"; + +export class ConfigurationProfileBuilder { + protected name: string = null; + protected pluginFactories: (typeof ConnectionPluginFactory)[] = null; + protected properties: Map = null; + protected databaseDialect: DatabaseDialect | (() => DatabaseDialect) = null; + protected driverDialect: DriverDialect | (() => DriverDialect) = null; + protected awsCredentialProvider: any | null = null; //AwsCredentialsProviderHandler + protected connectionProvider: ConnectionProvider | (() => ConnectionProvider) = null; + + private constructor() {} + + public static get(): ConfigurationProfileBuilder { + return new ConfigurationProfileBuilder(); + } + + public withName(name: string): ConfigurationProfileBuilder { + this.name = name; + return this; + } + + public withProperties(properties: Map): ConfigurationProfileBuilder { + this.properties = properties; + return this; + } + + public withPluginsFactories(pluginFactories: (typeof ConnectionPluginFactory)[]): ConfigurationProfileBuilder { + this.pluginFactories = pluginFactories; + return this; + } + + public withDatabaseDialect(databaseDialect: DatabaseDialect): ConfigurationProfileBuilder { + this.databaseDialect = databaseDialect; + return this; + } + + public withDriverDialect(driverDialect: DriverDialect): ConfigurationProfileBuilder { + this.driverDialect = driverDialect; + return this; + } + + public withConnectionProvider(connectionProvider: ConnectionProvider): ConfigurationProfileBuilder { + this.connectionProvider = connectionProvider; + return this; + } + + public withAwsCredentialProvider(awsCredentialProvider: any): ConfigurationProfileBuilder { + this.awsCredentialProvider = awsCredentialProvider; + return this; + } + + public from(presetProfileName: string): ConfigurationProfileBuilder { + const configurationProfile = DriverConfigurationProfiles.getProfileConfiguration(presetProfileName); + if (!configurationProfile) { + throw new AwsWrapperError(Messages.get("ConfigurationProfileBuilder.notFound", presetProfileName)); + } + + this.name = configurationProfile.getName(); + this.properties = configurationProfile.getProperties(); + this.databaseDialect = configurationProfile.getDatabaseDialect(); + this.driverDialect = configurationProfile.getDriverDialect(); + this.awsCredentialProvider = configurationProfile.getAwsCredentialProvider(); + this.connectionProvider = configurationProfile.getConnectionProvider(); + this.pluginFactories = configurationProfile.getPluginFactories(); + + return this; + } + + public build(): ConfigurationProfile { + if (!this.name || this.name.length === 0) { + throw new AwsWrapperError(Messages.get("ConfigurationProfileBuilder.profileNameRequired", this.name)); + } + if (ConfigurationProfilePresetCodes.isKnownPreset(this.name)) { + throw new AwsWrapperError(Messages.get("ConfigurationProfileBuilder.canNotUpdateKnownPreset", this.name)); + } + return new ConfigurationProfile( + this.name, + this.pluginFactories, + this.properties, + this.databaseDialect, + this.driverDialect, + this.awsCredentialProvider, + this.connectionProvider + ); + } + + public buildAndSet() { + DriverConfigurationProfiles.addOrReplaceProfile(this.name, this.build()); + } +} diff --git a/common/lib/profile/configuration_profile_codes.ts b/common/lib/profile/configuration_profile_codes.ts new file mode 100644 index 00000000..ddc34ea8 --- /dev/null +++ b/common/lib/profile/configuration_profile_codes.ts @@ -0,0 +1,38 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +export class ConfigurationProfilePresetCodes { + public static readonly A0 = "A0"; // Normal + public static readonly A1 = "A1"; // Easy + public static readonly A2 = "A2"; // Aggressive + public static readonly B = "B"; // Normal + public static readonly C0 = "C0"; // Normal + public static readonly C1 = "C1"; // Aggressive + public static readonly D0 = "D0"; // Normal + public static readonly D1 = "D1"; // Easy + public static readonly E = "E"; // Normal + public static readonly F0 = "F0"; // Normal + public static readonly F1 = "F1"; // Aggressive + public static readonly G0 = "G0"; // Normal + public static readonly G1 = "G1"; // Easy + public static readonly H = "H"; // Normal + public static readonly I0 = "I0"; // Normal + public static readonly I1 = "I1"; // Aggressive + + public static isKnownPreset(name: string): boolean { + return Object.prototype.hasOwnProperty.call(ConfigurationProfilePresetCodes, name); + } +} diff --git a/common/lib/profile/driver_configuration_profiles.ts b/common/lib/profile/driver_configuration_profiles.ts new file mode 100644 index 00000000..f00030ec --- /dev/null +++ b/common/lib/profile/driver_configuration_profiles.ts @@ -0,0 +1,416 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ConfigurationProfile } from "./configuration_profile"; +import { ConfigurationProfilePresetCodes } from "./configuration_profile_codes"; +import { WrapperProperties } from "../wrapper_property"; +import { HostMonitoringPluginFactory } from "../plugins/efm/host_monitoring_plugin_factory"; +import { AuroraInitialConnectionStrategyFactory } from "../plugins/aurora_initial_connection_strategy_plugin_factory"; +import { AuroraConnectionTrackerPluginFactory } from "../plugins/connection_tracker/aurora_connection_tracker_plugin_factory"; +import { ReadWriteSplittingPluginFactory } from "../plugins/read_write_splitting_plugin_factory"; +import { FailoverPluginFactory } from "../plugins/failover/failover_plugin_factory"; +import { InternalPooledConnectionProvider } from "../internal_pooled_connection_provider"; +import { AwsPoolConfig } from "../aws_pool_config"; +import { StaleDnsPluginFactory } from "../plugins/stale_dns/stale_dns_plugin_factory"; + +export class DriverConfigurationProfiles { + private static readonly MONITORING_CONNECTION_PREFIX = "monitoring-"; + private static readonly activeProfiles: Map = new Map(); + private static readonly presets: Map = new Map([ + [ + ConfigurationProfilePresetCodes.A0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.A0, + [], + new Map([ + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 5000], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + null + ) + ], + [ + ConfigurationProfilePresetCodes.A1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.A1, + [], + new Map([ + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 30000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 30000], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + null + ) + ], + [ + ConfigurationProfilePresetCodes.A2, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.A2, + [], + new Map([ + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 3000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 3000], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + null + ) + ], + [ + ConfigurationProfilePresetCodes.B, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.B, + [], + new Map([ + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 0], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: true }] + ]), + null, + null, + null, + null + ) + ], + [ + ConfigurationProfilePresetCodes.C0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.C0, + [HostMonitoringPluginFactory], // Factories should be presorted by weights! + new Map([ + [WrapperProperties.FAILURE_DETECTION_TIME_MS.name, 60000], + [WrapperProperties.FAILURE_DETECTION_COUNT.name, 5], + [WrapperProperties.FAILURE_DETECTION_INTERVAL_MS.name, 15000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 5000], + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 0], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + null + ) + ], + [ + ConfigurationProfilePresetCodes.C1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.C1, + [HostMonitoringPluginFactory], // Factories should be presorted by weights! + new Map([ + [WrapperProperties.FAILURE_DETECTION_TIME_MS.name, 30000], + [WrapperProperties.FAILURE_DETECTION_COUNT.name, 3], + [WrapperProperties.FAILURE_DETECTION_INTERVAL_MS.name, 5000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 3000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 3000], + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 0], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + null + ) + ], + [ + ConfigurationProfilePresetCodes.D0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.D0, + // Factories should be presorted by weights! + [AuroraInitialConnectionStrategyFactory, AuroraConnectionTrackerPluginFactory, ReadWriteSplittingPluginFactory, FailoverPluginFactory], + new Map([ + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 5000], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + () => { + return new InternalPooledConnectionProvider( + new AwsPoolConfig({ + maxConnections: 30, + maxIdleConnections: 2, + minConnections: 2, + idleTimeoutMillis: 15 * 60000, // 15min + allowExitOnIdle: true + }) + ); + } + ) + ], + [ + ConfigurationProfilePresetCodes.D1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.D1, + // Factories should be presorted by weights! + [AuroraInitialConnectionStrategyFactory, AuroraConnectionTrackerPluginFactory, ReadWriteSplittingPluginFactory, FailoverPluginFactory], + new Map([ + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 30000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 30000], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + () => { + return new InternalPooledConnectionProvider( + new AwsPoolConfig({ + maxConnections: 30, + maxIdleConnections: 2, + minConnections: 2, + idleTimeoutMillis: 15 * 60000, // 15min + allowExitOnIdle: true + }) + ); + } + ) + ], + [ + ConfigurationProfilePresetCodes.E, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.E, + // Factories should be presorted by weights! + [AuroraInitialConnectionStrategyFactory, AuroraConnectionTrackerPluginFactory, ReadWriteSplittingPluginFactory, FailoverPluginFactory], + new Map([ + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 0], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: true }] + ]), + null, + null, + null, + () => { + return new InternalPooledConnectionProvider( + new AwsPoolConfig({ + maxConnections: 30, + maxIdleConnections: 2, + minConnections: 2, + idleTimeoutMillis: 15 * 60000, // 15min + allowExitOnIdle: true + }) + ); + } + ) + ], + [ + ConfigurationProfilePresetCodes.F0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.F0, + // Factories should be presorted by weights! + [ + AuroraInitialConnectionStrategyFactory, + AuroraConnectionTrackerPluginFactory, + ReadWriteSplittingPluginFactory, + FailoverPluginFactory, + HostMonitoringPluginFactory + ], + new Map([ + [WrapperProperties.FAILURE_DETECTION_TIME_MS.name, 60000], + [WrapperProperties.FAILURE_DETECTION_COUNT.name, 5], + [WrapperProperties.FAILURE_DETECTION_INTERVAL_MS.name, 15000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 5000], + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 0], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + () => { + return new InternalPooledConnectionProvider( + new AwsPoolConfig({ + maxConnections: 30, + maxIdleConnections: 2, + minConnections: 2, + idleTimeoutMillis: 15 * 60000, // 15min + allowExitOnIdle: true + }) + ); + } + ) + ], + [ + ConfigurationProfilePresetCodes.F1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.F1, + // Factories should be presorted by weights! + [ + AuroraInitialConnectionStrategyFactory, + AuroraConnectionTrackerPluginFactory, + ReadWriteSplittingPluginFactory, + FailoverPluginFactory, + HostMonitoringPluginFactory + ], + new Map([ + [WrapperProperties.FAILURE_DETECTION_TIME_MS.name, 30000], + [WrapperProperties.FAILURE_DETECTION_COUNT.name, 3], + [WrapperProperties.FAILURE_DETECTION_INTERVAL_MS.name, 5000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 3000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 3000], + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 0], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + () => { + return new InternalPooledConnectionProvider( + new AwsPoolConfig({ + maxConnections: 30, + maxIdleConnections: 2, + minConnections: 2, + idleTimeoutMillis: 15 * 60000, // 15min + allowExitOnIdle: true + }) + ); + } + ) + ], + [ + ConfigurationProfilePresetCodes.G0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.G0, + // Factories should be presorted by weights! + [AuroraConnectionTrackerPluginFactory, StaleDnsPluginFactory, FailoverPluginFactory], + new Map([ + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 5000], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + null + ) + ], + [ + ConfigurationProfilePresetCodes.G1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.G1, + // Factories should be presorted by weights! + [AuroraConnectionTrackerPluginFactory, StaleDnsPluginFactory, FailoverPluginFactory], + new Map([ + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 30000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 30000], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + null + ) + ], + [ + ConfigurationProfilePresetCodes.H, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.H, + // Factories should be presorted by weights! + [AuroraConnectionTrackerPluginFactory, StaleDnsPluginFactory, FailoverPluginFactory], + new Map([ + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 0], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: true }] + ]), + null, + null, + null, + null + ) + ], + [ + ConfigurationProfilePresetCodes.I0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.I0, + // Factories should be presorted by weights! + [AuroraConnectionTrackerPluginFactory, StaleDnsPluginFactory, FailoverPluginFactory, HostMonitoringPluginFactory], + new Map([ + [WrapperProperties.FAILURE_DETECTION_TIME_MS.name, 60000], + [WrapperProperties.FAILURE_DETECTION_COUNT.name, 5], + [WrapperProperties.FAILURE_DETECTION_INTERVAL_MS.name, 15000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 5000], + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 0], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: true }] + ]), + null, + null, + null, + null + ) + ], + [ + ConfigurationProfilePresetCodes.I1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.I1, + // Factories should be presorted by weights! + [AuroraConnectionTrackerPluginFactory, StaleDnsPluginFactory, FailoverPluginFactory, HostMonitoringPluginFactory], + new Map([ + [WrapperProperties.FAILURE_DETECTION_TIME_MS.name, 30000], + [WrapperProperties.FAILURE_DETECTION_COUNT.name, 3], + [WrapperProperties.FAILURE_DETECTION_INTERVAL_MS.name, 5000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 3000], + [DriverConfigurationProfiles.MONITORING_CONNECTION_PREFIX + WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 3000], + [WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name, 10000], + [WrapperProperties.WRAPPER_QUERY_TIMEOUT.name, 0], + [WrapperProperties.KEEPALIVE_PROPERTIES.name, { keepAlive: false }] + ]), + null, + null, + null, + null + ) + ] + ]); + + public static clear() { + DriverConfigurationProfiles.clear(); + } + + public static addOrReplaceProfile(profileName: string, configurationProfile: ConfigurationProfile) { + DriverConfigurationProfiles.activeProfiles.set(profileName, configurationProfile); + } + + public static remove(profileName: string) { + DriverConfigurationProfiles.activeProfiles.delete(profileName); + } + + public static contains(profileName: string): boolean { + return DriverConfigurationProfiles.activeProfiles.has(profileName); + } + + public static getProfileConfiguration(profileName: string): ConfigurationProfile { + const profile: ConfigurationProfile = DriverConfigurationProfiles.activeProfiles.get(profileName); + if (profile) { + return profile; + } + return DriverConfigurationProfiles.presets.get(profileName); + } +} diff --git a/common/lib/utils/locales/en.json b/common/lib/utils/locales/en.json index b7e25320..1dd52e36 100644 --- a/common/lib/utils/locales/en.json +++ b/common/lib/utils/locales/en.json @@ -1,13 +1,15 @@ { "PluginManager.unknownPluginCode": "Unknown plugin code: '%s'", + "PluginManager.unknownPluginWeight": "Unknown plugin weight for %s.", "PluginManager.pipelineNone": "A pipeline was requested but the created pipeline evaluated to undefined.", "PluginManager.unableToRetrievePlugin": "Unable to retrieve plugin instance.", "ConnectionProvider.unsupportedHostSelectorStrategy": "Unsupported host selection strategy '%s' specified for this connection provider '%s'. Please visit the documentation for all supported strategies.", "ConnectionPluginChainBuilder.errorImportingPlugin": "The plugin could not be imported due to error '%s'. Please ensure the required dependencies have been installed. Plugin: '%s'", "ClientUtils.queryTaskTimeout": "Client query task timed out, if a network error did not occur, please review the usage of the 'mysqlQueryTimeout' connection parameter.", "ClientUtils.connectTimeout": "Client connect timed out.", - "DialectManager.unknownDialectCode": "Unknown dialect code: '%s'.", - "DialectManager.getDialectError": "Was not able to get a database dialect.", + "DatabaseDialectManager.unknownDialectCode": "Unknown dialect code: '%s'.", + "DatabaseDialectManager.getDialectError": "Was not able to get a database dialect.", + "DatabaseDialectManager.wrongCustomDialect": "Provided custom database dialect should implement DatabaseDialect.", "DefaultPlugin.executingMethod": "Executing method: %s", "DefaultConnectionPlugin.unknownRoleRequested": "A HostInfo with a role of HostRole.UNKNOWN was requested via getHostInfoByStrategy. The requested role must be either HostRole.WRITER or HostRole.READER", "DefaultConnectionPlugin.noHostsAvailable": "The default connection plugin received an empty host list from the plugin service.", @@ -181,10 +183,15 @@ "LimitlessRouterServiceImpl.getLimitlessRoutersException": "Exception encountered getting Limitless Routers. %s", "LimitlessRouterServiceImpl.fetchedEmptyRouterList": "Empty router list was fetched.", "LimitlessRouterServiceImpl.errorStartingMonitor": "An error occurred while starting Limitless Router Monitor. %s", + "AwsCredentialsManager.wrongHandler": "Provided AWS credential provider handler should implement AwsCredentialsProviderHandler.", "HostResponseTimeMonitor.stopped": "Host Response Time Monitor task stopped on instance '%s'.", "HostResponseTimeMonitor.responseTime": "Response time for '%s': '%s' ms.", "HostResponseTimeMonitor.interruptedErrorDuringMonitoring": "Response time task for host '%s' was interrupted.", "HostResponseTimeMonitor.openingConnection": "Opening a Response time connection to '%s'.", "HostResponseTimeMonitor.openedConnection": "Opened Response time connection: '%s'.", - "FastestResponseStrategyPlugin.unsupportedHostSelectorStrategy": "Unsupported host selector strategy: '%s'. To use the fastest response strategy plugin, please ensure the property 'readerHostSelectorStrategy' is set to 'fastestResponse'." + "FastestResponseStrategyPlugin.unsupportedHostSelectorStrategy": "Unsupported host selector strategy: '%s'. To use the fastest response strategy plugin, please ensure the property 'readerHostSelectorStrategy' is set to 'fastestResponse'.", + "ConfigurationProfileBuilder.notFound": "Configuration profile '%s' not found.", + "ConfigurationProfileBuilder.profileNameRequired": "Profile name is required.", + "ConfigurationProfileBuilder.canNotUpdateKnownPreset": "Can't add or update a built-in preset configuration profile '%s'.", + "AwsClient.configurationProfileNotFound": "Configuration profile '%s' not found." } diff --git a/common/lib/wrapper_property.ts b/common/lib/wrapper_property.ts index 9cf54b87..1ee6d04d 100644 --- a/common/lib/wrapper_property.ts +++ b/common/lib/wrapper_property.ts @@ -15,6 +15,7 @@ */ import { ConnectionProvider } from "./connection_provider"; +import { DatabaseDialect } from "./database_dialect/database_dialect"; export class WrapperProperty { name: string; @@ -356,7 +357,23 @@ export class WrapperProperties { null ); - static removeWrapperProperties(props: Map): any { + static readonly CUSTOM_DATABASE_DIALECT = new WrapperProperty( + "customDatabaseDialect", + "A reference to a custom database dialect object.", + null + ); + + static readonly CUSTOM_AWS_CREDENTIAL_PROVIDER_HANDLER = new WrapperProperty( + "customAwsCredentialProviderHandler", + "A reference to a custom AwsCredentialsProviderHandler object.", + null + ); + + static readonly AWS_PROFILE = new WrapperProperty("awsProfile", "Name of the AWS Profile to use for IAM or SecretsManager auth.", null); + + static readonly PROFILE_NAME = new WrapperProperty("profileName", "Driver configuration profile name", null); + + static removeWrapperProperties(props: Map): Map { const persistingProperties = [ WrapperProperties.USER.name, WrapperProperties.PASSWORD.name, @@ -384,6 +401,6 @@ export class WrapperProperties { } }); - return Object.fromEntries(copy.entries()); + return copy; } } diff --git a/docs/files/configuration-profile-presets.pdf b/docs/files/configuration-profile-presets.pdf new file mode 100644 index 00000000..2f06cff5 Binary files /dev/null and b/docs/files/configuration-profile-presets.pdf differ diff --git a/docs/images/configuration-presets.png b/docs/images/configuration-presets.png new file mode 100644 index 00000000..38ad7316 Binary files /dev/null and b/docs/images/configuration-presets.png differ diff --git a/docs/using-the-nodejs-wrapper/ConfigurationPresets.md b/docs/using-the-nodejs-wrapper/ConfigurationPresets.md new file mode 100644 index 00000000..85962853 --- /dev/null +++ b/docs/using-the-nodejs-wrapper/ConfigurationPresets.md @@ -0,0 +1,50 @@ +# Configuration Presets + +## What is a Configuration Preset? + +A Configuration Preset is a [configuration profile](./UsingTheNodejsWrapper.md#configuration-profiles) that has already been set up by the AWS Advanced NodeJS Wrapper team. Preset configuration profiles are optimized, profiled, verified and can be used right away. If the existing presets do not cover an exact use case, users can also create their own configuration profiles based on the built-in presets. + +## Using Configuration Presets + +The Configuration Preset name should be specified with the [`profileName`](./UsingTheNodejsWrapper.md#connection-plugin-manager-parameters) parameter. + +```typescript +const client = new AwsMySQLClient({ + ... + profileName: "A2" +}); +``` + +Users can create their own custom configuration profiles based on built-in configuration presets. + +Users can not delete built-in configuration presets. + +```typescript +// Create a new configuration profile "myNewProfile" based on "A2" configuration preset +ConfigurationProfileBuilder.get() + .from("A2") + .withName("myNewProfile") + .withDatabaseDialect(new CustomDatabaseDialect()) + .buildAndSet(); + +const client = new AwsMySQLClient({ + ... + profileName: "myNewProfile" +}); +``` + +## Existing Configuration Presets + +Configuration Presets are optimized for 3 main user scenarios. They are: + +- **No connection pool** preset family: `A`, `B`, `C` +- AWS Advanced NodeJS Wrapper **Internal connection pool** preset family: `D`, `E`, `F` +- **External connection pool** preset family: `G`, `H`, `I` + +Some preset names may include a number, like `A0`, `A1`, `A2`, `D0`, `D1`, etc. Usually, the number represent sensitivity or timing variations for the same preset. For example, `A0` is optimized for normal network outage sensitivity and normal response time, while `A1` is less sensitive. Please take into account that more aggressive presets tend to cause more false positive failure detections. More details can be found in this file: [configuration_profile_codes.ts](./../../common/lib/profile/configuration_profile_codes.ts) + +Choosing the right configuration preset for your application can be a challenging task. Many presets could potentially fit the needs of your application. Various user application requirements and goals are presented in the following table and organized to help you identify the most suitable presets for your application. + +PDF version of the following table can be found [here](./../files/configuration-profile-presets.pdf). + +
diff --git a/docs/using-the-nodejs-wrapper/DatabaseDialects.md b/docs/using-the-nodejs-wrapper/DatabaseDialects.md index 3b87a867..683e8369 100644 --- a/docs/using-the-nodejs-wrapper/DatabaseDialects.md +++ b/docs/using-the-nodejs-wrapper/DatabaseDialects.md @@ -33,7 +33,7 @@ Dialect codes specify what kind of database any connections will be made to. If you are interested in using the AWS Advanced NodeJS Wrapper but your desired database type is not currently supported, it is possible to create a custom dialect. -To create a custom dialect, implement the [`Dialect`](../../common/lib/database_dialect/database_dialect.ts) interface. For databases clusters that are aware of their topology, the [`TopologyAwareDatabaseDialect`](../../common/lib/topology_aware_database_dialect.ts) interface should also be implemented. For database clusters that use an [Aurora Limitless Database](../../docs/using-the-nodejs-wrapper/using-plugins/UsingTheLimitlessConnectionPlugin.md#what-is-amazon-aurora-limitless-database) then [`LimitlessDatabaseDialect`](../../common/lib/database_dialect/limitless_database_dialect.ts) should be implemented. +To create a custom dialect, implement the [`DatabaseDialect`](../../common/lib/database_dialect/database_dialect.ts) interface. For databases clusters that are aware of their topology, the [`TopologyAwareDatabaseDialect`](../../common/lib/topology_aware_database_dialect.ts) interface should also be implemented. For database clusters that use an [Aurora Limitless Database](../../docs/using-the-nodejs-wrapper/using-plugins/UsingTheLimitlessConnectionPlugin.md#what-is-amazon-aurora-limitless-database) then [`LimitlessDatabaseDialect`](../../common/lib/database_dialect/limitless_database_dialect.ts) should be implemented. See the following classes for examples: @@ -46,9 +46,15 @@ See the following classes for examples: - [AuroraMySQLDatabaseDialect](../../mysql/lib/dialect/aurora_mysql_database_dialect.ts) - This dialect is an extension of MySQLDatabaseDialect, but also implements the `TopologyAwareDatabaseDialect` interface. -Once the custom dialect class has been created, tell the AWS Advanced NodeJS Wrapper to use it with the `setCustomDialect` method in the `DialectManager` class. It is not necessary to set the `dialect` parameter. See below for an example: +Once the custom dialect class has been created, tell the AWS Advanced NodeJS Wrapper to use it by setting the `customDatabaseDialect` parameter. It is not necessary to set the `dialect` parameter in this case. See below for an example: ```typescript myDialect: DatabaseDialect = new CustomDialect(); -DialectManager.setCustomDialect(myDialect); + +const client = new AwsPGClient({ + ... + customDatabaseDialect: myDialect + ... +}); + ``` diff --git a/docs/using-the-nodejs-wrapper/UsingTheNodejsWrapper.md b/docs/using-the-nodejs-wrapper/UsingTheNodejsWrapper.md index 607fa2e8..452d4cff 100644 --- a/docs/using-the-nodejs-wrapper/UsingTheNodejsWrapper.md +++ b/docs/using-the-nodejs-wrapper/UsingTheNodejsWrapper.md @@ -40,20 +40,24 @@ To enable logging when using the AWS Advanced NodeJS Wrapper, use the `LOG_LEVEL These parameters are applicable to any instance of the AWS Advanced NodeJS Wrapper. -| Parameter | Value | Required | Description | Default Value | Version Supported | -| ------------------------------ | ------------------ | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | -| `host` | `string` | No | Database host. | `null` | `latest` | -| `database` | `string` | No | Database name. | `null` | `latest` | -| `user` | `string` | No | Database username. | `null` | `latest` | -| `password` | `string` | No | Database password. | `null` | `latest` | -| `transferSessionStateOnSwitch` | `boolean` | No | Enables transferring the session state to a new connection. | `true` | `latest` | -| `resetSessionStateOnClose` | `boolean` | No | Enables resetting the session state before closing connection. | `true` | `latest` | -| `enableGreenHostReplacement` | `boolean` | No | Enables replacing a green node host name with the original host name when the green host DNS doesn't exist anymore after a blue/green switchover. Refer to [Overview of Amazon RDS Blue/Green Deployments](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments-overview.html) for more details about green and blue nodes. | `false` | `latest` | -| `clusterInstanceHostPattern` | `string` | If connecting using an IP address or custom domain URL: Yes

Otherwise: No | This parameter is not required unless connecting to an AWS RDS cluster via an IP address or custom domain URL. In those cases, this parameter specifies the cluster instance DNS pattern that will be used to build a complete instance endpoint. A "?" character in this pattern should be used as a placeholder for the DB instance identifiers of the instances in the cluster. See [here](#host-pattern) for more information.

Example: `?.my-domain.com`, `any-subdomain.?.my-domain.com`

Use case Example: If your cluster instance endpoints follow this pattern:`instanceIdentifier1.customHost`, `instanceIdentifier2.customHost`, etc. and you want your initial connection to be to `customHost:1234`, then your client configuration should look like this: `{ host: "customHost", port: 1234, database: "test", clusterInstanceHostPattern: "?.customHost" }` | If the provided host is not an IP address or custom domain, the NodeJS Wrapper will automatically acquire the cluster instance host pattern from the customer-provided host. | `latest` | -| ~~`mysqlQueryTimeout`~~ | `number` | No | This parameter has been deprecated since version 1.1.0, applications should use the `wrapperQueryTimeout` parameter instead.

Query timeout in milliseconds. This is only applicable when using the AwsMySQLClient. To set query timeout for the AwsPGClient, please use the built-in `query_timeout` parameter. See the `node-postgres` [documentation](https://node-postgres.com/apis/client) for more details. | 20000 | `1.0.0` | -| `wrapperConnectTimeout` | `number` | No | Connect timeout in milliseconds. This parameter will apply the provided timeout value to the underlying driver's built-in connect timeout parameter, if there is one available. | 20000 | `latest` | -| `wrapperQueryTimeout` | `number` | No | Query timeout in milliseconds. This parameter will apply the provided timeout value to the underlying driver's built-in query timeout parameter, if there is one available. The wrapper will also use this value for its own query timeout implementation. | 20000 | `latest` | -| `wrapperKeepAliveProperties` | `Map` | No | If the underlying target driver has keepAlive properties available, properties within this map will be applied to the underlying target driver's client configuration. For example, the node-postgres driver's `keepAlive` and `keepAliveInitialDelayMillis` properties can be configured by setting this property in the client configuration: `{ wrapperKeepAliveProperties: new Map([["keepAlive", true], ["keepAliveInitialDelayMillis", 1234]]) }`.

Currently supported drivers: node-postgres | `null` | +| Parameter | Value | Required | Description | Default Value | Version Supported | +| ------------------------------------ | ------------------ | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | +| `host` | `string` | No | Database host. | `null` | `latest` | +| `database` | `string` | No | Database name. | `null` | `latest` | +| `user` | `string` | No | Database username. | `null` | `latest` | +| `password` | `string` | No | Database password. | `null` | `latest` | +| `transferSessionStateOnSwitch` | `boolean` | No | Enables transferring the session state to a new connection. | `true` | `latest` | +| `resetSessionStateOnClose` | `boolean` | No | Enables resetting the session state before closing connection. | `true` | `latest` | +| `enableGreenHostReplacement` | `boolean` | No | Enables replacing a green node host name with the original host name when the green host DNS doesn't exist anymore after a blue/green switchover. Refer to [Overview of Amazon RDS Blue/Green Deployments](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments-overview.html) for more details about green and blue nodes. | `false` | `latest` | +| `clusterInstanceHostPattern` | `string` | If connecting using an IP address or custom domain URL: Yes

Otherwise: No | This parameter is not required unless connecting to an AWS RDS cluster via an IP address or custom domain URL. In those cases, this parameter specifies the cluster instance DNS pattern that will be used to build a complete instance endpoint. A "?" character in this pattern should be used as a placeholder for the DB instance identifiers of the instances in the cluster. See [here](#host-pattern) for more information.

Example: `?.my-domain.com`, `any-subdomain.?.my-domain.com`

Use case Example: If your cluster instance endpoints follow this pattern:`instanceIdentifier1.customHost`, `instanceIdentifier2.customHost`, etc. and you want your initial connection to be to `customHost:1234`, then your client configuration should look like this: `{ host: "customHost", port: 1234, database: "test", clusterInstanceHostPattern: "?.customHost" }` | If the provided host is not an IP address or custom domain, the NodeJS Wrapper will automatically acquire the cluster instance host pattern from the customer-provided host. | `latest` | +| ~~`mysqlQueryTimeout`~~ | `number` | No | This parameter has been deprecated since version 1.1.0, applications should use the `wrapperQueryTimeout` parameter instead.

Query timeout in milliseconds. This is only applicable when using the AwsMySQLClient. To set query timeout for the AwsPGClient, please use the built-in `query_timeout` parameter. See the `node-postgres` [documentation](https://node-postgres.com/apis/client) for more details. | 20000 | `1.0.0` | +| `wrapperConnectTimeout` | `number` | No | Connect timeout in milliseconds. This parameter will apply the provided timeout value to the underlying driver's built-in connect timeout parameter, if there is one available. | 20000 | `latest` | +| `wrapperQueryTimeout` | `number` | No | Query timeout in milliseconds. This parameter will apply the provided timeout value to the underlying driver's built-in query timeout parameter, if there is one available. The wrapper will also use this value for its own query timeout implementation. | 20000 | `latest` | +| `wrapperKeepAliveProperties` | `Map` | No | If the underlying target driver has keepAlive properties available, properties within this map will be applied to the underlying target driver's client configuration. For example, the node-postgres driver's `keepAlive` and `keepAliveInitialDelayMillis` properties can be configured by setting this property in the client configuration: `{ wrapperKeepAliveProperties: new Map([["keepAlive", true], ["keepAliveInitialDelayMillis", 1234]]) }`.

Currently supported drivers: node-postgres | `null` | +| `awsProfile` | `string` | No | Allows users to specify a profile name for AWS credentials. This parameter is used by plugins that require AWS credentials, like the [AWS IAM Authentication Plugin](./using-plugins/UsingTheIamAuthenticationPlugin.md) and the [AWS Secrets Manager Plugin](./using-plugins/UsingTheAwsSecretsManagerPlugin.md). | `null` | +| `connectionProvider` | `object` | No | Allows users to specify a connection provider used to create connections. Provided value should be an object that implements `ConnectionProvider` interface. | `null` | +| `customDatabaseDialect` | `object` | No | Allows users to specify a custom database dialect. Provided value should be an object that implements `DatabaseDialect` interface. | `null` | +| `customAwsCredentialProviderHandler` | `object` | No | Allows users to specify a custom AWS credentials provider. This parameter is used by plugins that require AWS credentials, like the [AWS IAM Authentication Plugin](./using-plugins/UsingTheIamAuthenticationPlugin.md) and the [AWS Secrets Manager Plugin](./using-plugins/UsingTheAwsSecretsManagerPlugin.md). For more information see [AWS Credentials Provider Configuration](./custom-configuration/AwsCredentialsConfiguration.md). | `null` | ## Host Pattern @@ -72,10 +76,11 @@ Plugins are loaded and managed through the Connection Plugin Manager and may be ### Connection Plugin Manager Parameters -| Parameter | Value | Required | Description | Default Value | -| ---------------------------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | -| `plugins` | `String` | No | Comma separated list of connection plugin codes.

Example: `failover,efm` | `auroraConnectionTracker,failover,efm` | -| `autoSortWrapperPluginOrder` | `Boolean` | No | Allows the AWS Advanced NodeJS Wrapper to sort connection plugins to prevent plugin misconfiguration. Allows a user to provide a custom plugin order if needed. | `true` | +| Parameter | Value | Required | Description | Default Value | +| ---------------------------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | +| `plugins` | `String` | No | Comma separated list of connection plugin codes.

Example: `failover,efm` | `auroraConnectionTracker,failover,efm` | +| `autoSortWrapperPluginOrder` | `Boolean` | No | Allows the AWS Advanced NodeJS Wrapper to sort connection plugins to prevent plugin misconfiguration. Allows a user to provide a custom plugin order if needed. | `true` | +| `profileName` | `String` | No | Driver configuration profile name. Instead of listing plugin codes with `plugins`, the driver profile can be set with this parameter.

Example: See [below](#configuration-profiles). | `null` | To use a built-in plugin, specify its relevant plugin code for the `plugins` . The default value for `plugins` is `failover`. These plugins are enabled by default. To read more about these plugins, see the [List of Available Plugins](#list-of-available-plugins) section. @@ -130,3 +135,50 @@ The AWS Advanced NodeJS Wrapper has several built-in plugins that are available In addition to the built-in plugins, you can also create custom plugins more suitable for your needs. For more information, see [Custom Plugins](../development-guide/LoadablePlugins.md#using-custom-plugins). + +### Configuration Profiles + +An alternative way of loading plugins and providing configuration parameters is to use a configuration profile. You can create custom configuration profiles that specify which plugins the AWS Advanced NodeJS Wrapper should load. After creating the profile, set the [`profileName`](#connection-plugin-manager-parameters) parameter to the name of the created profile. +This method of loading plugins will most often be used by those who require custom plugins that cannot be loaded with the [`plugins`](#connection-plugin-manager-parameters) parameter, or by those who are using preset configurations. + +Besides a list of plugins to load and configuration properties, configuration profiles may also include the following items: + +- [Database Dialect](./DatabaseDialects.md#database-dialects) +- [Driver Dialect](../../common/lib/driver_dialect/driver_dialect.ts) +- a custom exception handler +- a custom connection provider + +The following example creates and sets a configuration profile: + +```typescript +// Create a new configuration profile with name "testProfile" +ConfigurationProfileBuilder.get() + .withName("testProfile") + .withPluginsFactories([FailoverPluginFactory, HostMonitoringPluginFactory, CustomConnectionPluginFactory]) + .buildAndSet(); + +// Use the configuration profile "testProfile" +const client = new AwsMySQLClient({ + user: "user", + password: "password", + host: "host", + database: "database", + profileName: "testProfile" +}); +``` + +Configuration profiles can be created based on other existing configuration profiles. Profile names are case sensitive and should be unique. + +```typescript +// Create a new configuration profile with name "newProfile" based on "existingProfileName" +ConfigurationProfileBuilder.get() + .from("existingProfileName") + .withName("newProfileName") + .withDatabaseDialect(new CustomDatabaseDialect()) + .buildAndSet(); + +// Delete configuration profile "testProfile" +DriverConfigurationProfiles.remove("testProfile"); +``` + +The AWS Advanced NodeJS Wrapper team has gathered and analyzed various user scenarios to create commonly used configuration profiles, or presets, for users. These preset configuration profiles are optimized, profiled, verified and can be used right away. Users can create their own configuration profiles based on the built-in presets as shown above. More details could be found at the [Configuration Presets](./ConfigurationPresets.md) page. diff --git a/docs/using-the-nodejs-wrapper/custom-configuration/AwsCredentialsConfiguration.md b/docs/using-the-nodejs-wrapper/custom-configuration/AwsCredentialsConfiguration.md new file mode 100644 index 00000000..e6d2eec3 --- /dev/null +++ b/docs/using-the-nodejs-wrapper/custom-configuration/AwsCredentialsConfiguration.md @@ -0,0 +1,27 @@ +# AWS Credentials Provider Configuration + +### Applicable plugins: AWS IAM Authentication Plugin, AWS Secrets Manager Plugin + +The [AWS IAM Authentication Plugin](../using-plugins/UsingTheIamAuthenticationPlugin.md) and [AWS Secrets Manager Plugin](../using-plugins/UsingTheAwsSecretsManagerPlugin.md) both require authentication via AWS credentials to provide the functionality they offer. In the plugin logic, the mechanism to locate your credentials is defined by passing in an `AwsCredentialsProvider` object to the applicable AWS SDK client. By default, an instance of `DefaultCredentialsProvider` will be passed, which locates your credentials using the default credential provider chain described [in this doc](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/). If AWS credentials are provided by the `credentials` and `config` files ([Default credentials provider chain](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/#fromini)), then it's possible to specify a profile name using the `awsProfile` configuration parameter. If no profile name is specified, a `[default]` profile is used. + +If you would like to define your own mechanism for providing AWS credentials, you can do so using `customAwsCredentialProviderHandler` parameter for a new connection and passing an object that implements `AwsCredentialsProviderHandler`. See below for an example: + +```typescript +import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; + +class MyCustomAwsCredentialProvider implements AwsCredentialsProviderHandler { + getAwsCredentialsProvider(hostInfo: HostInfo, properties: Map): AwsCredentialIdentityProvider { + // Initialize AWS Credential Provider here and return it. + // The following code is just an example. + return fromNodeProviderChain(); + } +} +myProvider: MyCustomAwsCredentialProvider = new MyCustomAwsCredentialProvider(); + +const client = new AwsPGClient({ + ... + customAwsCredentialProviderHandler: myProvider + ... +}); + +``` diff --git a/docs/using-the-nodejs-wrapper/using-plugins/UsingTheAwsSecretsManagerPlugin.md b/docs/using-the-nodejs-wrapper/using-plugins/UsingTheAwsSecretsManagerPlugin.md index 76fb0466..f4753345 100644 --- a/docs/using-the-nodejs-wrapper/using-plugins/UsingTheAwsSecretsManagerPlugin.md +++ b/docs/using-the-nodejs-wrapper/using-plugins/UsingTheAwsSecretsManagerPlugin.md @@ -11,7 +11,7 @@ The AWS Advanced NodeJS Wrapper supports usage of database credentials stored as To enable the AWS Secrets Manager Connection Plugin, add the plugin code `secretsManager` to the [`plugins`](../UsingTheNodejsWrapper.md#connection-plugin-manager-parameters) connection parameter. -This plugin requires a valid set of AWS credentials to retrieve the database credentials from AWS Secrets Manager. The AWS credentials must be located in [one of these locations](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/#fromNodeProviderChain) supported by the AWS SDK's default credentials provider. +This plugin requires a valid set of AWS credentials to retrieve the database credentials from AWS Secrets Manager. The AWS credentials must be located in [one of these locations](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/#fromNodeProviderChain) supported by the AWS SDK's default credentials provider. See also at [AWS Credentials Configuration](../custom-configuration/AwsCredentialsConfiguration.md) ## AWS Secrets Manager Connection Plugin Parameters diff --git a/docs/using-the-nodejs-wrapper/using-plugins/UsingTheIamAuthenticationPlugin.md b/docs/using-the-nodejs-wrapper/using-plugins/UsingTheIamAuthenticationPlugin.md index 979c41c5..da8b2057 100644 --- a/docs/using-the-nodejs-wrapper/using-plugins/UsingTheIamAuthenticationPlugin.md +++ b/docs/using-the-nodejs-wrapper/using-plugins/UsingTheIamAuthenticationPlugin.md @@ -36,6 +36,8 @@ The AWS Advanced NodeJS Wrapper supports Amazon AWS Identity and Access Manageme | `iamRegion` | `String` | No | This property will override the default region that is used to generate the IAM token. If the property is not set, the wrapper will attempt to parse the region from the host provided in the configuration parameters. | `null` | `us-east-2` | | `iamTokenExpiration` | `Number` | No | This property determines how long an IAM token is kept in the driver cache before a new one is generated. The default expiration time is set to be 15 minutes. Note that IAM database authentication tokens have a lifetime of 15 minutes. | `900` | `600` | +This plugin requires a valid set of AWS credentials to retrieve the database credentials from AWS Secrets Manager. The AWS credentials must be located in [one of these locations](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/#fromNodeProviderChain) supported by the AWS SDK's default credentials provider. See also at [AWS Credentials Configuration](../custom-configuration/AwsCredentialsConfiguration.md) + ## Using the IAM Authentication Plugin with Custom Endpoints When using AWS IAM database authentication with a custom domain or an IP address, in addition to the `clusterInstanceHostPattern` variable, the `iamHost` must be specified and must point to a valid Amazon endpoint, i.e. `db-identifier.cluster-XYZ.us-east-2.rds.amazonaws.com`. diff --git a/mysql/lib/dialect/mysql2_driver_dialect.ts b/mysql/lib/dialect/mysql2_driver_dialect.ts index 302094f6..7ba6323a 100644 --- a/mysql/lib/dialect/mysql2_driver_dialect.ts +++ b/mysql/lib/dialect/mysql2_driver_dialect.ts @@ -26,9 +26,10 @@ import { HostInfo } from "../../../common/lib/host_info"; import { UnsupportedMethodError } from "../../../common/lib/utils/errors"; export class MySQL2DriverDialect implements DriverDialect { - static readonly connectTimeoutPropertyName = "connectTimeout"; - static readonly queryTimeoutPropertyName = "timeout"; protected dialectName: string = this.constructor.name; + private static readonly CONNECT_TIMEOUT_PROPERTY_NAME = "connectTimeout"; + private static readonly QUERY_TIMEOUT_PROPERTY_NAME = "timeout"; + private static readonly KEEP_ALIVE_PROPERTY_NAME = "keepAlive"; getDialectName(): string { return this.dialectName; @@ -36,10 +37,10 @@ export class MySQL2DriverDialect implements DriverDialect { async connect(hostInfo: HostInfo, props: Map): Promise { const driverProperties = WrapperProperties.removeWrapperProperties(props); - // MySQL2 does not support keep alive, explicitly check and throw an exception if this value is set. + // MySQL2 does not support keep alive, explicitly check and throw an exception if this value is set to true. this.setKeepAliveProperties(driverProperties, props.get(WrapperProperties.KEEPALIVE_PROPERTIES.name)); this.setConnectTimeout(driverProperties, props.get(WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name)); - const targetClient = await createConnection(driverProperties); + const targetClient = await createConnection(Object.fromEntries(driverProperties.entries())); return Promise.resolve(new MySQLClientWrapper(targetClient, hostInfo, props, this)); } @@ -63,19 +64,19 @@ export class MySQL2DriverDialect implements DriverDialect { setConnectTimeout(props: Map, wrapperConnectTimeout?: any) { const timeout = wrapperConnectTimeout ?? props.get(WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name); if (timeout) { - props.set(MySQL2DriverDialect.connectTimeoutPropertyName, timeout); + props.set(MySQL2DriverDialect.CONNECT_TIMEOUT_PROPERTY_NAME, timeout); } } setQueryTimeout(props: Map, sql?: any, wrapperConnectTimeout?: any) { const timeout = wrapperConnectTimeout ?? props.get(WrapperProperties.WRAPPER_QUERY_TIMEOUT.name); - if (timeout && !sql[MySQL2DriverDialect.queryTimeoutPropertyName]) { - sql[MySQL2DriverDialect.queryTimeoutPropertyName] = timeout; + if (timeout && !sql[MySQL2DriverDialect.QUERY_TIMEOUT_PROPERTY_NAME]) { + sql[MySQL2DriverDialect.QUERY_TIMEOUT_PROPERTY_NAME] = timeout; } } setKeepAliveProperties(props: Map, keepAliveProps: any) { - if (keepAliveProps) { + if (keepAliveProps && keepAliveProps.get(MySQL2DriverDialect.KEEP_ALIVE_PROPERTY_NAME)) { throw new UnsupportedMethodError("Keep alive configuration is not supported for MySQL2."); } } diff --git a/pg/lib/dialect/node_postgres_driver_dialect.ts b/pg/lib/dialect/node_postgres_driver_dialect.ts index 6f10f1e5..15656d75 100644 --- a/pg/lib/dialect/node_postgres_driver_dialect.ts +++ b/pg/lib/dialect/node_postgres_driver_dialect.ts @@ -27,11 +27,11 @@ import { PgClientWrapper } from "../../../common/lib/pg_client_wrapper"; import { HostInfo } from "../../../common/lib/host_info"; export class NodePostgresDriverDialect implements DriverDialect { - static readonly connectTimeoutPropertyName = "connectionTimeoutMillis"; - static readonly queryTimeoutPropertyName = "query_timeout"; protected dialectName: string = this.constructor.name; - private static keepAlivePropertyName = "keepAlive"; - private static keepAliveInitialDelayMillisPropertyName = "keepAliveInitialDelayMillis"; + private static readonly CONNECT_TIMEOUT_PROPERTY_NAME = "connectionTimeoutMillis"; + private static readonly QUERY_TIMEOUT_PROPERTY_NAME = "query_timeout"; + private static readonly KEEP_ALIVE_PROPERTY_NAME = "keepAlive"; + private static readonly KEEP_ALIVE_INITIAL_DELAY_MILLIS_PROPERTY_NAME = "keepAliveInitialDelayMillis"; getDialectName(): string { return this.dialectName; @@ -42,7 +42,7 @@ export class NodePostgresDriverDialect implements DriverDialect { this.setKeepAliveProperties(driverProperties, props.get(WrapperProperties.KEEPALIVE_PROPERTIES.name)); this.setConnectTimeout(driverProperties, props.get(WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name)); this.setQueryTimeout(driverProperties, props.get(WrapperProperties.WRAPPER_QUERY_TIMEOUT.name)); - const targetClient = new pkgPg.Client(driverProperties); + const targetClient = new pkgPg.Client(Object.fromEntries(driverProperties.entries())); await targetClient.connect(); return Promise.resolve(new PgClientWrapper(targetClient, hostInfo, props)); } @@ -67,14 +67,14 @@ export class NodePostgresDriverDialect implements DriverDialect { setConnectTimeout(props: Map, wrapperConnectTimeout?: any) { const timeout = wrapperConnectTimeout ?? props.get(WrapperProperties.WRAPPER_CONNECT_TIMEOUT.name); if (timeout) { - props.set(NodePostgresDriverDialect.connectTimeoutPropertyName, timeout); + props.set(NodePostgresDriverDialect.CONNECT_TIMEOUT_PROPERTY_NAME, timeout); } } setQueryTimeout(props: Map, sql?: any, wrapperQueryTimeout?: any) { const timeout = wrapperQueryTimeout ?? props.get(WrapperProperties.WRAPPER_QUERY_TIMEOUT.name); if (timeout) { - props.set(NodePostgresDriverDialect.queryTimeoutPropertyName, timeout); + props.set(NodePostgresDriverDialect.QUERY_TIMEOUT_PROPERTY_NAME, timeout); } } @@ -83,14 +83,14 @@ export class NodePostgresDriverDialect implements DriverDialect { return; } - const keepAlive = keepAliveProps.get(NodePostgresDriverDialect.keepAlivePropertyName); - const keepAliveInitialDelayMillis = keepAliveProps.get(NodePostgresDriverDialect.keepAliveInitialDelayMillisPropertyName); + const keepAlive = keepAliveProps.get(NodePostgresDriverDialect.KEEP_ALIVE_PROPERTY_NAME); + const keepAliveInitialDelayMillis = keepAliveProps.get(NodePostgresDriverDialect.KEEP_ALIVE_INITIAL_DELAY_MILLIS_PROPERTY_NAME); if (keepAlive) { - props.set(NodePostgresDriverDialect.keepAlivePropertyName, keepAlive); + props.set(NodePostgresDriverDialect.KEEP_ALIVE_PROPERTY_NAME, keepAlive); } if (keepAliveInitialDelayMillis) { - props.set(NodePostgresDriverDialect.keepAliveInitialDelayMillisPropertyName, keepAliveInitialDelayMillis); + props.set(NodePostgresDriverDialect.KEEP_ALIVE_INITIAL_DELAY_MILLIS_PROPERTY_NAME, keepAliveInitialDelayMillis); } } } diff --git a/tests/integration/container/tests/iam_authentication.test.ts b/tests/integration/container/tests/iam_authentication.test.ts index 94540e95..62d57cf5 100644 --- a/tests/integration/container/tests/iam_authentication.test.ts +++ b/tests/integration/container/tests/iam_authentication.test.ts @@ -27,6 +27,7 @@ import { logger } from "../../../../common/logutils"; import { TestEnvironmentFeatures } from "./utils/test_environment_features"; import { features } from "./config"; import { PluginManager } from "../../../../common/lib"; +import { jest } from "@jest/globals"; const itIf = !features.includes(TestEnvironmentFeatures.PERFORMANCE) && @@ -83,6 +84,7 @@ async function validateConnection(client: AwsPGClient | AwsMySQLClient) { describe("iam authentication", () => { beforeEach(async () => { logger.info(`Test started: ${expect.getState().currentTestName}`); + jest.useFakeTimers(); env = await TestEnvironment.getCurrent(); driver = DriverHelper.getDriverForDatabaseEngine(env.engine); initClientFunc = DriverHelper.getClient(driver); diff --git a/tests/unit/aws_client_get_plugin_instance.test.ts b/tests/unit/aws_client_get_plugin_instance.test.ts index b8a32b86..fc7272b4 100644 --- a/tests/unit/aws_client_get_plugin_instance.test.ts +++ b/tests/unit/aws_client_get_plugin_instance.test.ts @@ -29,7 +29,7 @@ class DevPluginTest extends DeveloperConnectionPlugin { class TestClient extends AwsPGClient { setManager() { - this.pluginManager.init([plugin]); + this.pluginManager.init(null, [plugin]); } } diff --git a/tests/unit/database_dialect.test.ts b/tests/unit/database_dialect.test.ts index 4b631dc0..17084135 100644 --- a/tests/unit/database_dialect.test.ts +++ b/tests/unit/database_dialect.test.ts @@ -278,7 +278,8 @@ describe("test database dialects", () => { databaseType, expectedDialect!.dialects, props, - mockDriverDialect + mockDriverDialect, + null ); await pluginService.updateDialect(mockClientWrapper); expect(pluginService.getDialect()).toBe(expectedDialectClass);