Skip to content

Commit f9ce248

Browse files
authored
feat: Configuration profiles (#338)
1 parent f91e999 commit f9ce248

25 files changed

Lines changed: 1078 additions & 124 deletions

common/lib/authentication/aws_credentials_manager.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,34 @@
1717
import { HostInfo } from "../host_info";
1818
import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
1919
import { AwsCredentialIdentityProvider } from "@smithy/types/dist-types/identity/awsCredentialIdentity";
20+
import { WrapperProperties } from "../wrapper_property";
21+
import { AwsWrapperError } from "../utils/errors";
22+
import { Messages } from "../utils/messages";
2023

2124
interface AwsCredentialsProviderHandler {
2225
getAwsCredentialsProvider(hostInfo: HostInfo, properties: Map<string, any>): AwsCredentialIdentityProvider;
2326
}
2427

2528
export class AwsCredentialsManager {
26-
private static handler?: AwsCredentialsProviderHandler;
27-
28-
static setCustomHandler(customHandler: AwsCredentialsProviderHandler) {
29-
AwsCredentialsManager.handler = customHandler;
30-
}
31-
3229
static getProvider(hostInfo: HostInfo, props: Map<string, any>): AwsCredentialIdentityProvider {
33-
return AwsCredentialsManager.handler === undefined
34-
? AwsCredentialsManager.getDefaultProvider()
35-
: AwsCredentialsManager.handler.getAwsCredentialsProvider(hostInfo, props);
36-
}
30+
const awsCredentialProviderHandler = WrapperProperties.CUSTOM_AWS_CREDENTIAL_PROVIDER_HANDLER.get(props);
31+
if (awsCredentialProviderHandler && !AwsCredentialsManager.isAwsCredentialsProviderHandler(awsCredentialProviderHandler)) {
32+
throw new AwsWrapperError(Messages.get("AwsCredentialsManager.wrongHandler"));
33+
}
3734

38-
static resetCustomHandler() {
39-
AwsCredentialsManager.handler = undefined;
35+
return !awsCredentialProviderHandler
36+
? AwsCredentialsManager.getDefaultProvider(WrapperProperties.AWS_PROFILE.get(props))
37+
: awsCredentialProviderHandler.getAwsCredentialsProvider(hostInfo, props);
4038
}
4139

42-
private static getDefaultProvider() {
40+
private static getDefaultProvider(profileName: string | null) {
41+
if (profileName) {
42+
return fromNodeProviderChain({ profile: profileName });
43+
}
4344
return fromNodeProviderChain();
4445
}
46+
47+
private static isAwsCredentialsProviderHandler(arg: any): arg is AwsCredentialsProviderHandler {
48+
return arg.getAwsCredentialsProvider !== undefined;
49+
}
4550
}

common/lib/aws_client.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ import { DefaultTelemetryFactory } from "./utils/telemetry/default_telemetry_fac
3131
import { TelemetryFactory } from "./utils/telemetry/telemetry_factory";
3232
import { DriverDialect } from "./driver_dialect/driver_dialect";
3333
import { WrapperProperties } from "./wrapper_property";
34+
import { DriverConfigurationProfiles } from "./profile/driver_configuration_profiles";
35+
import { ConfigurationProfile } from "./profile/configuration_profile";
36+
import { AwsWrapperError } from "./utils/errors";
37+
import { Messages } from "./utils/messages";
3438

3539
export abstract class AwsClient extends EventEmitter {
3640
private _defaultPort: number = -1;
@@ -41,6 +45,7 @@ export abstract class AwsClient extends EventEmitter {
4145
protected _isReadOnly: boolean = false;
4246
protected _isolationLevel: number = 0;
4347
protected _connectionUrlParser: ConnectionUrlParser;
48+
protected _configurationProfile: ConfigurationProfile | null = null;
4449
readonly properties: Map<string, any>;
4550
config: any;
4651
targetClient?: ClientWrapper;
@@ -58,9 +63,50 @@ export abstract class AwsClient extends EventEmitter {
5863

5964
this.properties = new Map<string, any>(Object.entries(config));
6065

66+
const profileName = WrapperProperties.PROFILE_NAME.get(this.properties);
67+
if (profileName && profileName.length > 0) {
68+
this._configurationProfile = DriverConfigurationProfiles.getProfileConfiguration(profileName);
69+
if (this._configurationProfile) {
70+
const profileProperties = this._configurationProfile.getProperties();
71+
if (profileProperties) {
72+
for (const key of profileProperties.keys()) {
73+
if (this.properties.has(key)) {
74+
// Setting defined by a user has priority over property in configuration profile.
75+
continue;
76+
}
77+
this.properties.set(key, profileProperties.get(key));
78+
}
79+
80+
const connectionProvider = WrapperProperties.CONNECTION_PROVIDER.get(this.properties);
81+
if (!connectionProvider) {
82+
WrapperProperties.CONNECTION_PROVIDER.set(this.properties, this._configurationProfile.getAwsCredentialProvider());
83+
}
84+
85+
const customAwsCredentialProvider = WrapperProperties.CUSTOM_AWS_CREDENTIAL_PROVIDER_HANDLER.get(this.properties);
86+
if (!customAwsCredentialProvider) {
87+
WrapperProperties.CUSTOM_AWS_CREDENTIAL_PROVIDER_HANDLER.set(this.properties, this._configurationProfile.getAwsCredentialProvider());
88+
}
89+
90+
const customDatabaseDialect = WrapperProperties.CUSTOM_DATABASE_DIALECT.get(this.properties);
91+
if (!customDatabaseDialect) {
92+
WrapperProperties.CUSTOM_DATABASE_DIALECT.set(this.properties, this._configurationProfile.getDatabaseDialect());
93+
}
94+
}
95+
} else {
96+
throw new AwsWrapperError(Messages.get("AwsClient.configurationProfileNotFound", profileName));
97+
}
98+
}
99+
61100
this.telemetryFactory = new DefaultTelemetryFactory(this.properties);
62101
const container = new PluginServiceManagerContainer();
63-
this.pluginService = new PluginService(container, this, dbType, knownDialectsByCode, this.properties, driverDialect);
102+
this.pluginService = new PluginService(
103+
container,
104+
this,
105+
dbType,
106+
knownDialectsByCode,
107+
this.properties,
108+
this._configurationProfile?.getDriverDialect() ?? driverDialect
109+
);
64110
this.pluginManager = new PluginManager(
65111
container,
66112
this.properties,
@@ -71,7 +117,7 @@ export abstract class AwsClient extends EventEmitter {
71117

72118
private async setup() {
73119
await this.telemetryFactory.init();
74-
await this.pluginManager.init();
120+
await this.pluginManager.init(this._configurationProfile);
75121
}
76122

77123
protected async internalConnect() {

common/lib/connection_plugin_chain_builder.ts

Lines changed: 70 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { DeveloperConnectionPluginFactory } from "./plugins/dev/developer_connec
3838
import { ConnectionPluginFactory } from "./plugin_factory";
3939
import { LimitlessConnectionPluginFactory } from "./plugins/limitless/limitless_connection_plugin_factory";
4040
import { FastestResponseStrategyPluginFactory } from "./plugins/strategy/fastest_response/fastest_respose_strategy_plugin_factory";
41+
import { ConfigurationProfile } from "./profile/configuration_profile";
4142

4243
/*
4344
Type alias used for plugin factory sorting. It holds a reference to a plugin
@@ -69,58 +70,90 @@ export class ConnectionPluginChainBuilder {
6970
["executeTime", { factory: ExecuteTimePluginFactory, weight: ConnectionPluginChainBuilder.WEIGHT_RELATIVE_TO_PRIOR_PLUGIN }]
7071
]);
7172

73+
static readonly PLUGIN_WEIGHTS = new Map<typeof ConnectionPluginFactory, number>([
74+
[AuroraInitialConnectionStrategyFactory, 390],
75+
[AuroraConnectionTrackerPluginFactory, 400],
76+
[StaleDnsPluginFactory, 500],
77+
[ReadWriteSplittingPluginFactory, 600],
78+
[FailoverPluginFactory, 700],
79+
[HostMonitoringPluginFactory, 800],
80+
[LimitlessConnectionPluginFactory, 950],
81+
[IamAuthenticationPluginFactory, 1000],
82+
[AwsSecretsManagerPluginFactory, 1100],
83+
[FederatedAuthPluginFactory, 1200],
84+
[OktaAuthPluginFactory, 1300],
85+
[DeveloperConnectionPluginFactory, 1400],
86+
[ConnectTimePluginFactory, ConnectionPluginChainBuilder.WEIGHT_RELATIVE_TO_PRIOR_PLUGIN],
87+
[ExecuteTimePluginFactory, ConnectionPluginChainBuilder.WEIGHT_RELATIVE_TO_PRIOR_PLUGIN]
88+
]);
89+
7290
static async getPlugins(
7391
pluginService: PluginService,
7492
props: Map<string, any>,
75-
connectionProviderManager: ConnectionProviderManager
93+
connectionProviderManager: ConnectionProviderManager,
94+
configurationProfile: ConfigurationProfile | null
7695
): Promise<ConnectionPlugin[]> {
96+
let pluginFactoryInfoList: PluginFactoryInfo[] = [];
7797
const plugins: ConnectionPlugin[] = [];
78-
let pluginCodes: string = props.get(WrapperProperties.PLUGINS.name);
79-
if (pluginCodes == null) {
80-
pluginCodes = WrapperProperties.DEFAULT_PLUGINS;
81-
}
82-
83-
const usingDefault = pluginCodes === WrapperProperties.DEFAULT_PLUGINS;
98+
let usingDefault: boolean = false;
8499

85-
pluginCodes = pluginCodes.trim();
86-
87-
if (pluginCodes !== "") {
88-
const pluginCodeList = pluginCodes.split(",").map((pluginCode) => pluginCode.trim());
89-
let pluginFactoryInfoList: PluginFactoryInfo[] = [];
90-
let lastWeight = 0;
91-
pluginCodeList.forEach((p) => {
92-
if (!ConnectionPluginChainBuilder.PLUGIN_FACTORIES.has(p)) {
93-
throw new AwsWrapperError(Messages.get("PluginManager.unknownPluginCode", p));
94-
}
95-
96-
const factoryInfo = ConnectionPluginChainBuilder.PLUGIN_FACTORIES.get(p);
97-
if (factoryInfo) {
98-
if (factoryInfo.weight === ConnectionPluginChainBuilder.WEIGHT_RELATIVE_TO_PRIOR_PLUGIN) {
99-
lastWeight++;
100-
} else {
101-
lastWeight = factoryInfo.weight;
100+
if (configurationProfile) {
101+
const profilePluginFactories = configurationProfile.getPluginFactories();
102+
if (profilePluginFactories) {
103+
for (const factory of profilePluginFactories) {
104+
const weight = ConnectionPluginChainBuilder.PLUGIN_WEIGHTS.get(factory);
105+
if (!weight) {
106+
throw new AwsWrapperError(Messages.get("PluginManager.unknownPluginWeight", factory.prototype.constructor.name));
102107
}
103-
pluginFactoryInfoList.push({ factory: factoryInfo.factory, weight: lastWeight });
108+
pluginFactoryInfoList.push({ factory: factory, weight: weight });
104109
}
105-
});
110+
usingDefault = true; // We assume that plugin factories in configuration profile is presorted.
111+
}
112+
} else {
113+
let pluginCodes: string = props.get(WrapperProperties.PLUGINS.name);
114+
if (pluginCodes == null) {
115+
pluginCodes = WrapperProperties.DEFAULT_PLUGINS;
116+
}
117+
usingDefault = pluginCodes === WrapperProperties.DEFAULT_PLUGINS;
106118

107-
if (!usingDefault && pluginFactoryInfoList.length > 1 && WrapperProperties.AUTO_SORT_PLUGIN_ORDER.get(props)) {
108-
pluginFactoryInfoList = pluginFactoryInfoList.sort((a, b) => a.weight - b.weight);
119+
pluginCodes = pluginCodes.trim();
120+
if (pluginCodes !== "") {
121+
const pluginCodeList = pluginCodes.split(",").map((pluginCode) => pluginCode.trim());
122+
let lastWeight = 0;
123+
pluginCodeList.forEach((p) => {
124+
if (!ConnectionPluginChainBuilder.PLUGIN_FACTORIES.has(p)) {
125+
throw new AwsWrapperError(Messages.get("PluginManager.unknownPluginCode", p));
126+
}
109127

110-
if (!usingDefault) {
111-
logger.info(
112-
"Plugins order has been rearranged. The following order is in effect: " +
113-
pluginFactoryInfoList.map((pluginFactoryInfo) => pluginFactoryInfo.factory.name.split("Factory")[0]).join(", ")
114-
);
115-
}
128+
const factoryInfo = ConnectionPluginChainBuilder.PLUGIN_FACTORIES.get(p);
129+
if (factoryInfo) {
130+
if (factoryInfo.weight === ConnectionPluginChainBuilder.WEIGHT_RELATIVE_TO_PRIOR_PLUGIN) {
131+
lastWeight++;
132+
} else {
133+
lastWeight = factoryInfo.weight;
134+
}
135+
pluginFactoryInfoList.push({ factory: factoryInfo.factory, weight: lastWeight });
136+
}
137+
});
116138
}
139+
}
140+
141+
if (!usingDefault && pluginFactoryInfoList.length > 1 && WrapperProperties.AUTO_SORT_PLUGIN_ORDER.get(props)) {
142+
pluginFactoryInfoList = pluginFactoryInfoList.sort((a, b) => a.weight - b.weight);
117143

118-
for (const pluginFactoryInfo of pluginFactoryInfoList) {
119-
const factoryObj = new pluginFactoryInfo.factory();
120-
plugins.push(await factoryObj.getInstance(pluginService, props));
144+
if (!usingDefault) {
145+
logger.info(
146+
"Plugins order has been rearranged. The following order is in effect: " +
147+
pluginFactoryInfoList.map((pluginFactoryInfo) => pluginFactoryInfo.factory.name.split("Factory")[0]).join(", ")
148+
);
121149
}
122150
}
123151

152+
for (const pluginFactoryInfo of pluginFactoryInfoList) {
153+
const factoryObj = new pluginFactoryInfo.factory();
154+
plugins.push(await factoryObj.getInstance(pluginService, props));
155+
}
156+
124157
plugins.push(new DefaultPlugin(pluginService, connectionProviderManager));
125158

126159
return plugins;

common/lib/database_dialect/database_dialect_manager.ts

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,43 +34,46 @@ export class DatabaseDialectManager implements DatabaseDialectProvider {
3434
*/
3535
private static readonly ENDPOINT_CACHE_EXPIRATION_MS = 86_400_000_000_000; // 24 hours
3636
protected static readonly knownEndpointDialects: CacheMap<string, string> = new CacheMap();
37-
protected readonly knownDialectsByCode: Map<string, DatabaseDialect>;
3837

39-
private static customDialect: DatabaseDialect | null = null;
40-
private readonly rdsHelper: RdsUtils = new RdsUtils();
41-
private readonly dbType;
42-
private canUpdate: boolean = false;
43-
private dialect: DatabaseDialect;
44-
private dialectCode: string = "";
38+
protected readonly knownDialectsByCode: Map<string, DatabaseDialect>;
39+
protected readonly customDialect: DatabaseDialect | null;
40+
protected readonly rdsHelper: RdsUtils = new RdsUtils();
41+
protected readonly dbType: DatabaseType;
42+
protected canUpdate: boolean = false;
43+
protected dialect: DatabaseDialect;
44+
protected dialectCode: string = "";
4545

4646
constructor(knownDialectsByCode: any, dbType: DatabaseType, props: Map<string, any>) {
4747
this.knownDialectsByCode = knownDialectsByCode;
4848
this.dbType = dbType;
49-
this.dialect = this.getDialect(props);
50-
}
5149

52-
static setCustomDialect(dialect: DatabaseDialect) {
53-
DatabaseDialectManager.customDialect = dialect;
54-
}
50+
const dialectSetting = WrapperProperties.CUSTOM_DATABASE_DIALECT.get(props);
51+
if (dialectSetting && !this.isDatabaseDialect(dialectSetting)) {
52+
throw new AwsWrapperError(Messages.get("DatabaseDialectManager.wrongCustomDialect"));
53+
}
54+
this.customDialect = dialectSetting;
5555

56-
static resetCustomDialect() {
57-
DatabaseDialectManager.customDialect = null;
56+
this.dialect = this.getDialect(props);
5857
}
5958

6059
static resetEndpointCache() {
6160
DatabaseDialectManager.knownEndpointDialects.clear();
6261
}
6362

63+
protected isDatabaseDialect(arg: any): arg is DatabaseDialect {
64+
return arg.getDialectName !== undefined;
65+
}
66+
6467
getDialect(props: Map<string, any>): DatabaseDialect {
6568
if (this.dialect) {
6669
return this.dialect;
6770
}
6871

6972
this.canUpdate = false;
7073

71-
if (DatabaseDialectManager.customDialect) {
74+
if (this.customDialect) {
7275
this.dialectCode = DatabaseDialectCodes.CUSTOM;
73-
this.dialect = DatabaseDialectManager.customDialect;
76+
this.dialect = this.customDialect;
7477
this.logCurrentDialect();
7578
return this.dialect;
7679
}
@@ -87,7 +90,7 @@ export class DatabaseDialectManager implements DatabaseDialectProvider {
8790
this.logCurrentDialect();
8891
return userDialect;
8992
}
90-
throw new AwsWrapperError(Messages.get("DialectManager.unknownDialectCode", dialectCode));
93+
throw new AwsWrapperError(Messages.get("DatabaseDialectManager.unknownDialectCode", dialectCode));
9194
}
9295

9396
if (this.dbType === DatabaseType.MYSQL) {
@@ -148,7 +151,7 @@ export class DatabaseDialectManager implements DatabaseDialectProvider {
148151
return this.dialect;
149152
}
150153

151-
throw new AwsWrapperError(Messages.get("DialectManager.getDialectError"));
154+
throw new AwsWrapperError(Messages.get("DatabaseDialectManager.getDialectError"));
152155
}
153156

154157
async getDialectForUpdate(targetClient: ClientWrapper, originalHost: string, newHost: string): Promise<DatabaseDialect> {
@@ -161,7 +164,7 @@ export class DatabaseDialectManager implements DatabaseDialectProvider {
161164
for (const dialectCandidateCode of dialectCandidates) {
162165
const dialectCandidate = this.knownDialectsByCode.get(dialectCandidateCode);
163166
if (!dialectCandidate) {
164-
throw new AwsWrapperError(Messages.get("DialectManager.unknownDialectCode", dialectCandidateCode));
167+
throw new AwsWrapperError(Messages.get("DatabaseDialectManager.unknownDialectCode", dialectCandidateCode));
165168
}
166169

167170
const isDialect = await dialectCandidate.isDialect(targetClient);

common/lib/plugin_manager.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { TelemetryFactory } from "./utils/telemetry/telemetry_factory";
3131
import { TelemetryTraceLevel } from "./utils/telemetry/telemetry_trace_level";
3232
import { ConnectionProvider } from "./connection_provider";
3333
import { ConnectionPluginFactory } from "./plugin_factory";
34+
import { ConfigurationProfile } from "./profile/configuration_profile";
3435

3536
type PluginFunc<T> = (plugin: ConnectionPlugin, targetFunc: () => Promise<T>) => Promise<T>;
3637

@@ -91,17 +92,18 @@ export class PluginManager {
9192
this.telemetryFactory = telemetryFactory;
9293
}
9394

94-
async init(): Promise<void>;
95-
async init(plugins: ConnectionPlugin[]): Promise<void>;
96-
async init(plugins?: ConnectionPlugin[]) {
95+
async init(configurationProfile?: ConfigurationProfile | null): Promise<void>;
96+
async init(configurationProfile: ConfigurationProfile | null, plugins: ConnectionPlugin[]): Promise<void>;
97+
async init(configurationProfile: ConfigurationProfile | null, plugins?: ConnectionPlugin[]) {
9798
if (this.pluginServiceManagerContainer.pluginService != null) {
9899
if (plugins) {
99100
this._plugins = plugins;
100101
} else {
101102
this._plugins = await ConnectionPluginChainBuilder.getPlugins(
102103
this.pluginServiceManagerContainer.pluginService,
103104
this.props,
104-
this.connectionProviderManager
105+
this.connectionProviderManager,
106+
configurationProfile
105107
);
106108
}
107109
}

0 commit comments

Comments
 (0)