From c7b1212a4354180663e9b23dda6c93566936c054 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Wed, 6 Jan 2021 21:15:29 -0800 Subject: [PATCH] AWS RDS token based password provider (#9518) * refresh db pwd * aws iam token password provider * fix analyze-dependencies build * fix doc build * add ut for BasicDataSourceExt * more doc updates * more doc update * moving aws token password provider to new extension * remove duplicate changes * make all config inline * extension docs * refresh db password in SQL Firehose code path as well * add ut * fix build * add new extension to distribution * rds lib is not provided * fix license build * add version to license * change parent version to 0.19.0-snapshot * address review comments * fix core/ code coverage * Update server/src/main/java/org/apache/druid/metadata/BasicDataSourceExt.java Co-authored-by: Clint Wylie * address review comments * fix spellchecker * remove inadvertant website file change Co-authored-by: Clint Wylie --- distribution/pom.xml | 2 + .../extensions-core/druid-aws-rds.md | 38 ++++ docs/development/extensions.md | 1 + .../druid-aws-rds-extensions/pom.xml | 80 ++++++++ .../apache/druid/aws/rds/AWSRDSModule.java | 47 +++++ .../aws/rds/AWSRDSTokenPasswordProvider.java | 123 ++++++++++++ ...rg.apache.druid.initialization.DruidModule | 16 ++ .../rds/AWSRDSTokenPasswordProviderTest.java | 82 ++++++++ licenses.yaml | 10 + pom.xml | 4 +- server/pom.xml | 11 ++ .../druid/metadata/BasicDataSourceExt.java | 179 ++++++++++++++++++ .../SQLFirehoseDatabaseConnector.java | 2 +- .../druid/metadata/SQLMetadataConnector.java | 2 +- .../metadata/BasicDataSourceExtTest.java | 113 +++++++++++ website/.spelling | 3 + 16 files changed, 710 insertions(+), 3 deletions(-) create mode 100644 docs/development/extensions-core/druid-aws-rds.md create mode 100644 extensions-core/druid-aws-rds-extensions/pom.xml create mode 100644 extensions-core/druid-aws-rds-extensions/src/main/java/org/apache/druid/aws/rds/AWSRDSModule.java create mode 100644 extensions-core/druid-aws-rds-extensions/src/main/java/org/apache/druid/aws/rds/AWSRDSTokenPasswordProvider.java create mode 100644 extensions-core/druid-aws-rds-extensions/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule create mode 100644 extensions-core/druid-aws-rds-extensions/src/test/java/org/apache/druid/aws/rds/AWSRDSTokenPasswordProviderTest.java create mode 100644 server/src/main/java/org/apache/druid/metadata/BasicDataSourceExt.java create mode 100644 server/src/test/java/org/apache/druid/metadata/BasicDataSourceExtTest.java diff --git a/distribution/pom.xml b/distribution/pom.xml index 75d5f11c71a..2753bad9b85 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -230,6 +230,8 @@ -c org.apache.druid.extensions:druid-s3-extensions -c + org.apache.druid.extensions:druid-aws-rds-extensions + -c org.apache.druid.extensions:druid-ec2-extensions -c org.apache.druid.extensions:druid-google-extensions diff --git a/docs/development/extensions-core/druid-aws-rds.md b/docs/development/extensions-core/druid-aws-rds.md new file mode 100644 index 00000000000..6bb0cd826be --- /dev/null +++ b/docs/development/extensions-core/druid-aws-rds.md @@ -0,0 +1,38 @@ +--- +id: druid-aws-rds +title: "Druid AWS RDS Module" +--- + + + +[AWS RDS](https://aws.amazon.com/rds/) is a managed service to operate relation databases such as PostgreSQL, Mysql etc. These databases could be accessed using static db password mechanism or via [AWS IAM](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html) temporary tokens. This module provides AWS RDS token [password provider](../../operations/password-provider.md) implementation to be used with [mysql-metadata-store](mysql.md) or [postgresql-metadata-store](postgresql.md) when mysql/postgresql is operated using AWS RDS. + +```json +{ "type": "aws-rds-token", "user": "USER", "host": "HOST", "port": PORT, "region": "AWS_REGION" } +``` + +Before using this password provider, please make sure that you have connected all dots for db user to connect using token. +See [AWS Guide](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.html). + +To use this extension, make sure you [include](../../development/extensions.md#loading-extensions) it in your config file along with other extensions e.g. + +``` +druid.extensions.loadList=["druid-aws-rds-extensions", "postgresql-metadata-storage", ...] +``` diff --git a/docs/development/extensions.md b/docs/development/extensions.md index 8d9bdb13927..e233bdd2677 100644 --- a/docs/development/extensions.md +++ b/docs/development/extensions.md @@ -56,6 +56,7 @@ Core extensions are maintained by Druid committers. |druid-ranger-security|Support for access control through Apache Ranger.|[link](../development/extensions-core/druid-ranger-security.md)| |druid-s3-extensions|Interfacing with data in AWS S3, and using S3 as deep storage.|[link](../development/extensions-core/s3.md)| |druid-ec2-extensions|Interfacing with AWS EC2 for autoscaling middle managers|UNDOCUMENTED| +|druid-aws-rds-extensions|Support for AWS token based access to AWS RDS DB Cluster.|[link](../development/extensions-core/druid-aws-rds.md)| |druid-stats|Statistics related module including variance and standard deviation.|[link](../development/extensions-core/stats.md)| |mysql-metadata-storage|MySQL metadata store.|[link](../development/extensions-core/mysql.md)| |postgresql-metadata-storage|PostgreSQL metadata store.|[link](../development/extensions-core/postgresql.md)| diff --git a/extensions-core/druid-aws-rds-extensions/pom.xml b/extensions-core/druid-aws-rds-extensions/pom.xml new file mode 100644 index 00000000000..397039e36ea --- /dev/null +++ b/extensions-core/druid-aws-rds-extensions/pom.xml @@ -0,0 +1,80 @@ + + + + + 4.0.0 + + org.apache.druid.extensions + druid-aws-rds-extensions + druid-aws-rds-extensions + druid-aws-rds-extensions + + + org.apache.druid + druid + 0.21.0-SNAPSHOT + ../../pom.xml + + + + + org.apache.druid + druid-core + ${project.parent.version} + provided + + + com.amazonaws + aws-java-sdk-rds + ${aws.sdk.version} + + + com.google.guava + guava + provided + + + com.google.inject + guice + provided + + + com.amazonaws + aws-java-sdk-core + provided + + + com.fasterxml.jackson.core + jackson-databind + provided + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + junit + junit + test + + + diff --git a/extensions-core/druid-aws-rds-extensions/src/main/java/org/apache/druid/aws/rds/AWSRDSModule.java b/extensions-core/druid-aws-rds-extensions/src/main/java/org/apache/druid/aws/rds/AWSRDSModule.java new file mode 100644 index 00000000000..298792a09b6 --- /dev/null +++ b/extensions-core/druid-aws-rds-extensions/src/main/java/org/apache/druid/aws/rds/AWSRDSModule.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.druid.aws.rds; + +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.common.collect.ImmutableList; +import com.google.inject.Binder; +import org.apache.druid.initialization.DruidModule; + +import java.util.List; + +public class AWSRDSModule implements DruidModule +{ + @Override + public List getJacksonModules() + { + return ImmutableList.of( + new SimpleModule("DruidAwsRdsExtentionsModule").registerSubtypes( + new NamedType(AWSRDSTokenPasswordProvider.class, "aws-rds-token") + ) + ); + } + + @Override + public void configure(Binder binder) + { + } +} diff --git a/extensions-core/druid-aws-rds-extensions/src/main/java/org/apache/druid/aws/rds/AWSRDSTokenPasswordProvider.java b/extensions-core/druid-aws-rds-extensions/src/main/java/org/apache/druid/aws/rds/AWSRDSTokenPasswordProvider.java new file mode 100644 index 00000000000..9c54d45124c --- /dev/null +++ b/extensions-core/druid-aws-rds-extensions/src/main/java/org/apache/druid/aws/rds/AWSRDSTokenPasswordProvider.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.druid.aws.rds; + +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.services.rds.auth.GetIamAuthTokenRequest; +import com.amazonaws.services.rds.auth.RdsIamAuthTokenGenerator; +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import org.apache.druid.java.util.common.RE; +import org.apache.druid.java.util.common.logger.Logger; +import org.apache.druid.metadata.PasswordProvider; + +/** + * Generates the AWS token same as aws cli + * aws rds generate-db-auth-token --hostname HOST --port PORT --region REGION --username USER + * and returns that as password. + * + * Before using this, please make sure that you have connected all dots for db user to connect using token. + * See https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.html + */ +public class AWSRDSTokenPasswordProvider implements PasswordProvider +{ + private static final Logger LOGGER = new Logger(AWSRDSTokenPasswordProvider.class); + private final String user; + private final String host; + private final int port; + private final String region; + + private final AWSCredentialsProvider awsCredentialsProvider; + + @JsonCreator + public AWSRDSTokenPasswordProvider( + @JsonProperty("user") String user, + @JsonProperty("host") String host, + @JsonProperty("port") int port, + @JsonProperty("region") String region, + @JacksonInject AWSCredentialsProvider awsCredentialsProvider + ) + { + this.user = Preconditions.checkNotNull(user, "null metadataStorage user"); + this.host = Preconditions.checkNotNull(host, "null metadataStorage host"); + Preconditions.checkArgument(port > 0, "must provide port"); + this.port = port; + + this.region = Preconditions.checkNotNull(region, "null region"); + + LOGGER.info("AWS RDS Config user[%s], host[%s], port[%d], region[%s]", this.user, this.host, port, this.region); + this.awsCredentialsProvider = Preconditions.checkNotNull(awsCredentialsProvider, "null AWSCredentialsProvider"); + } + + @JsonProperty + public String getUser() + { + return user; + } + + @JsonProperty + public String getHost() + { + return host; + } + + @JsonProperty + public int getPort() + { + return port; + } + + @JsonProperty + public String getRegion() + { + return region; + } + + @JsonIgnore + @Override + public String getPassword() + { + try { + RdsIamAuthTokenGenerator generator = RdsIamAuthTokenGenerator + .builder() + .credentials(awsCredentialsProvider) + .region(region) + .build(); + + String authToken = generator.getAuthToken( + GetIamAuthTokenRequest + .builder() + .hostname(host) + .port(port) + .userName(user) + .build() + ); + + return authToken; + } + catch (Exception ex) { + LOGGER.error(ex, "Couldn't generate AWS token."); + throw new RE(ex, "Couldn't generate AWS token."); + } + } +} diff --git a/extensions-core/druid-aws-rds-extensions/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule b/extensions-core/druid-aws-rds-extensions/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule new file mode 100644 index 00000000000..93b5bbc38fa --- /dev/null +++ b/extensions-core/druid-aws-rds-extensions/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +org.apache.druid.aws.rds.AWSRDSModule diff --git a/extensions-core/druid-aws-rds-extensions/src/test/java/org/apache/druid/aws/rds/AWSRDSTokenPasswordProviderTest.java b/extensions-core/druid-aws-rds-extensions/src/test/java/org/apache/druid/aws/rds/AWSRDSTokenPasswordProviderTest.java new file mode 100644 index 00000000000..c49a7862cbc --- /dev/null +++ b/extensions-core/druid-aws-rds-extensions/src/test/java/org/apache/druid/aws/rds/AWSRDSTokenPasswordProviderTest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.druid.aws.rds; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.druid.metadata.PasswordProvider; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; + +public class AWSRDSTokenPasswordProviderTest +{ + @Test + public void testSerde() throws IOException + { + ObjectMapper jsonMapper = new ObjectMapper(); + + for (Module module : new AWSRDSModule().getJacksonModules()) { + jsonMapper.registerModule(module); + } + + jsonMapper.setInjectableValues( + new InjectableValues.Std().addValue(AWSCredentialsProvider.class, new AWSCredentialsProvider() + { + @Override + public AWSCredentials getCredentials() + { + return null; + } + + @Override + public void refresh() + { + + } + }) + ); + + String jsonStr = "{\n" + + " \"type\": \"aws-rds-token\",\n" + + " \"user\": \"testuser\",\n" + + " \"host\": \"testhost\",\n" + + " \"port\": 5273,\n" + + " \"region\": \"testregion\"\n" + + "}\n"; + + PasswordProvider pp = jsonMapper.readValue( + jsonMapper.writeValueAsString( + jsonMapper.readValue(jsonStr, PasswordProvider.class) + ), + PasswordProvider.class + ); + + AWSRDSTokenPasswordProvider awsPwdProvider = (AWSRDSTokenPasswordProvider) pp; + Assert.assertEquals("testuser", awsPwdProvider.getUser()); + Assert.assertEquals("testhost", awsPwdProvider.getHost()); + Assert.assertEquals(5273, awsPwdProvider.getPort()); + Assert.assertEquals("testregion", awsPwdProvider.getRegion()); + } +} diff --git a/licenses.yaml b/licenses.yaml index d368c5f4787..a85a62e512a 100644 --- a/licenses.yaml +++ b/licenses.yaml @@ -147,6 +147,16 @@ source_paths: --- +name: AWS RDS SDK for Java +license_category: source +module: extensions/druid-aws-rds-extensions +license_name: Apache License version 2.0 +version: 1.11.199 +libraries: + - com.amazonaws: aws-java-sdk-rds + +--- + name: LDAP string encoding function from OWASP ESAPI license_category: source module: extensions/druid-basic-security diff --git a/pom.xml b/pom.xml index 023c90cdc15..812c2267eab 100644 --- a/pom.xml +++ b/pom.xml @@ -112,6 +112,7 @@ 2.0.2 1.11.199 2.8.0 + 0.8.5 @@ -168,6 +169,7 @@ extensions-core/lookups-cached-single extensions-core/ec2-extensions extensions-core/s3-extensions + extensions-core/druid-aws-rds-extensions extensions-core/simple-client-sslcontext extensions-core/druid-basic-security extensions-core/google-extensions @@ -1269,7 +1271,7 @@ org.jacoco jacoco-maven-plugin - 0.8.5 + ${jacoco.version} diff --git a/server/pom.xml b/server/pom.xml index 01b0d034d25..414b5f24e56 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -459,6 +459,17 @@ + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + + org/apache/druid/metadata/BasicDataSourceExt.class + + + org.apache.maven.plugins maven-jar-plugin diff --git a/server/src/main/java/org/apache/druid/metadata/BasicDataSourceExt.java b/server/src/main/java/org/apache/druid/metadata/BasicDataSourceExt.java new file mode 100644 index 00000000000..e077aae55f9 --- /dev/null +++ b/server/src/main/java/org/apache/druid/metadata/BasicDataSourceExt.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.druid.metadata; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.dbcp2.ConnectionFactory; +import org.apache.druid.java.util.common.RE; +import org.apache.druid.java.util.common.logger.Logger; + +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +/** + * This class exists so that {@link MetadataStorageConnectorConfig} is asked for password every time a brand new + * connection is established with DB. {@link PasswordProvider} impls such as those based on AWS tokens refresh the + * underlying token periodically since each token is valid for a certain period of time only. + * So, This class overrides (and largely copies due to lack of extensibility), the methods from base class in order to keep + * track of connection properties and call {@link MetadataStorageConnectorConfig#getPassword()} everytime a new + * connection is setup. + */ +public class BasicDataSourceExt extends BasicDataSource +{ + private static final Logger LOGGER = new Logger(BasicDataSourceExt.class); + + private Properties connectionProperties; + private final MetadataStorageConnectorConfig connectorConfig; + + public BasicDataSourceExt(MetadataStorageConnectorConfig connectorConfig) + { + this.connectorConfig = connectorConfig; + this.connectionProperties = new Properties(); + } + + @Override + public void addConnectionProperty(String name, String value) + { + connectionProperties.put(name, value); + super.addConnectionProperty(name, value); + } + + @Override + public void removeConnectionProperty(String name) + { + connectionProperties.remove(name); + super.removeConnectionProperty(name); + } + + @Override + public void setConnectionProperties(String connectionProperties) + { + if (connectionProperties == null) { + throw new NullPointerException("connectionProperties is null"); + } + + String[] entries = connectionProperties.split(";"); + Properties properties = new Properties(); + for (String entry : entries) { + if (entry.length() > 0) { + int index = entry.indexOf('='); + if (index > 0) { + String name = entry.substring(0, index); + String value = entry.substring(index + 1); + properties.setProperty(name, value); + } else { + // no value is empty string which is how java.util.Properties works + properties.setProperty(entry, ""); + } + } + } + this.connectionProperties = properties; + super.setConnectionProperties(connectionProperties); + } + + @VisibleForTesting + public Properties getConnectionProperties() + { + return connectionProperties; + } + + @Override + protected ConnectionFactory createConnectionFactory() throws SQLException + { + Driver driverToUse = getDriver(); + + if (driverToUse == null) { + Class driverFromCCL = null; + if (getDriverClassName() != null) { + try { + try { + if (getDriverClassLoader() == null) { + driverFromCCL = Class.forName(getDriverClassName()); + } else { + driverFromCCL = Class.forName( + getDriverClassName(), true, getDriverClassLoader()); + } + } + catch (ClassNotFoundException cnfe) { + driverFromCCL = Thread.currentThread( + ).getContextClassLoader().loadClass( + getDriverClassName()); + } + } + catch (Exception t) { + String message = "Cannot load JDBC driver class '" + + getDriverClassName() + "'"; + LOGGER.error(t, message); + throw new SQLException(message, t); + } + } + + try { + if (driverFromCCL == null) { + driverToUse = DriverManager.getDriver(getUrl()); + } else { + // Usage of DriverManager is not possible, as it does not + // respect the ContextClassLoader + // N.B. This cast may cause ClassCastException which is handled below + driverToUse = (Driver) driverFromCCL.newInstance(); + if (!driverToUse.acceptsURL(getUrl())) { + throw new SQLException("No suitable driver", "08001"); + } + } + } + catch (Exception t) { + String message = "Cannot create JDBC driver of class '" + + (getDriverClassName() != null ? getDriverClassName() : "") + + "' for connect URL '" + getUrl() + "'"; + LOGGER.error(t, message); + throw new SQLException(message, t); + } + } + + if (driverToUse == null) { + throw new RE("Failed to find the DB Driver"); + } + + final Driver finalDriverToUse = driverToUse; + + return () -> { + String user = connectorConfig.getUser(); + if (user != null) { + connectionProperties.put("user", user); + } else { + log("DBCP DataSource configured without a 'username'"); + } + + // Note: This is the main point of this class where we are getting fresh password before setting up + // every new connection. + String password = connectorConfig.getPassword(); + if (password != null) { + connectionProperties.put("password", password); + } else { + log("DBCP DataSource configured without a 'password'"); + } + + return finalDriverToUse.connect(connectorConfig.getConnectURI(), connectionProperties); + }; + } +} diff --git a/server/src/main/java/org/apache/druid/metadata/SQLFirehoseDatabaseConnector.java b/server/src/main/java/org/apache/druid/metadata/SQLFirehoseDatabaseConnector.java index 242f04bccfc..f72d9ae1fb5 100644 --- a/server/src/main/java/org/apache/druid/metadata/SQLFirehoseDatabaseConnector.java +++ b/server/src/main/java/org/apache/druid/metadata/SQLFirehoseDatabaseConnector.java @@ -64,7 +64,7 @@ public abstract class SQLFirehoseDatabaseConnector protected BasicDataSource getDatasource(MetadataStorageConnectorConfig connectorConfig) { - BasicDataSource dataSource = new BasicDataSource(); + BasicDataSource dataSource = new BasicDataSourceExt(connectorConfig); dataSource.setUsername(connectorConfig.getUser()); dataSource.setPassword(connectorConfig.getPassword()); String uri = connectorConfig.getConnectURI(); diff --git a/server/src/main/java/org/apache/druid/metadata/SQLMetadataConnector.java b/server/src/main/java/org/apache/druid/metadata/SQLMetadataConnector.java index 1e193886b8d..c41ca075fa3 100644 --- a/server/src/main/java/org/apache/druid/metadata/SQLMetadataConnector.java +++ b/server/src/main/java/org/apache/druid/metadata/SQLMetadataConnector.java @@ -654,7 +654,7 @@ public abstract class SQLMetadataConnector implements MetadataStorageConnector if (dbcpProperties != null) { dataSource = BasicDataSourceFactory.createDataSource(dbcpProperties); } else { - dataSource = new BasicDataSource(); + dataSource = new BasicDataSourceExt(connectorConfig); } } catch (Exception e) { diff --git a/server/src/test/java/org/apache/druid/metadata/BasicDataSourceExtTest.java b/server/src/test/java/org/apache/druid/metadata/BasicDataSourceExtTest.java new file mode 100644 index 00000000000..1f8af8f6c98 --- /dev/null +++ b/server/src/test/java/org/apache/druid/metadata/BasicDataSourceExtTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.druid.metadata; + +import org.apache.commons.dbcp2.ConnectionFactory; +import org.assertj.core.util.Lists; +import org.easymock.Capture; +import org.easymock.EasyMock; +import org.junit.Assert; +import org.junit.Test; + +import java.sql.Driver; +import java.util.List; +import java.util.Properties; + +public class BasicDataSourceExtTest +{ + @Test + public void testCreateConnectionFactory() throws Exception + { + MetadataStorageConnectorConfig connectorConfig = new MetadataStorageConnectorConfig() + { + private final List passwords = Lists.newArrayList("pwd1", "pwd2"); + + @Override + public String getUser() + { + return "testuser"; + } + + @Override + public String getPassword() + { + return passwords.remove(0); + } + }; + + BasicDataSourceExt basicDataSourceExt = new BasicDataSourceExt(connectorConfig); + + basicDataSourceExt.setConnectionProperties("p1=v1"); + basicDataSourceExt.addConnectionProperty("p2", "v2"); + + Driver driver = EasyMock.mock(Driver.class); + Capture uriArg = Capture.newInstance(); + Capture propsArg = Capture.newInstance(); + EasyMock.expect(driver.connect(EasyMock.capture(uriArg), EasyMock.capture(propsArg))).andReturn(null).times(2); + EasyMock.replay(driver); + + basicDataSourceExt.setDriver(driver); + + ConnectionFactory connectionFactory = basicDataSourceExt.createConnectionFactory(); + + Properties expectedProps = new Properties(); + expectedProps.put("p1", "v1"); + expectedProps.put("p2", "v2"); + expectedProps.put("user", connectorConfig.getUser()); + + + Assert.assertNull(connectionFactory.createConnection()); + Assert.assertEquals(connectorConfig.getConnectURI(), uriArg.getValue()); + + expectedProps.put("password", "pwd1"); + Assert.assertEquals(expectedProps, propsArg.getValue()); + + Assert.assertNull(connectionFactory.createConnection()); + Assert.assertEquals(connectorConfig.getConnectURI(), uriArg.getValue()); + + expectedProps.put("password", "pwd2"); + Assert.assertEquals(expectedProps, propsArg.getValue()); + } + + @Test + public void testConnectionPropertiesHanding() + { + BasicDataSourceExt basicDataSourceExt = new BasicDataSourceExt(EasyMock.mock(MetadataStorageConnectorConfig.class)); + Properties expectedProps = new Properties(); + + basicDataSourceExt.setConnectionProperties(""); + Assert.assertEquals(expectedProps, basicDataSourceExt.getConnectionProperties()); + + basicDataSourceExt.setConnectionProperties("p0;p1=v1;p2=v2;p3=v3"); + basicDataSourceExt.addConnectionProperty("p4", "v4"); + basicDataSourceExt.addConnectionProperty("p5", "v5"); + basicDataSourceExt.removeConnectionProperty("p2"); + basicDataSourceExt.removeConnectionProperty("p5"); + + expectedProps.put("p0", ""); + expectedProps.put("p1", "v1"); + expectedProps.put("p3", "v3"); + expectedProps.put("p4", "v4"); + + Assert.assertEquals(expectedProps, basicDataSourceExt.getConnectionProperties()); + + + } +} diff --git a/website/.spelling b/website/.spelling index 2d7baf9a060..4184c674101 100644 --- a/website/.spelling +++ b/website/.spelling @@ -88,6 +88,7 @@ HLL HashSet Homebrew HyperLogLog +IAM IANA IETF IP @@ -152,6 +153,7 @@ ParseSpecs Protobuf RDBMS RDDs +RDS Rackspace Redis S3 @@ -872,6 +874,7 @@ DistinctCount artifactId com.example common.runtime.properties +druid-aws-rds-extensions druid-cassandra-storage druid-distinctcount druid-ec2-extensions