Skip to content

Commit

Permalink
add initial database implementation
Browse files Browse the repository at this point in the history
this already allows connecting to OpenSearch using basic authentication.

note: the `liquibase.nosql` package has been copied from
[`liquibase-mongodb`] and adapted where needed (it is not 100% generic).
no authorship is claimed for this content!

[`liquibase-mongodb`]: https://github.com/liquibase/liquibase-mongodb
  • Loading branch information
rursprung authored and filipelautert committed May 14, 2024
1 parent ef873a6 commit 7ea8322
Show file tree
Hide file tree
Showing 10 changed files with 831 additions and 0 deletions.
62 changes: 62 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,74 @@
<artifactId>liquibase-core</artifactId>
<version>4.27.0</version>
</dependency>
<dependency>
<groupId>org.opensearch.client</groupId>
<artifactId>opensearch-java</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.opensearch</groupId>
<artifactId>opensearch-testcontainers</artifactId>
<version>2.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.13</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package liquibase.ext.opensearch.database;

import liquibase.Scope;
import liquibase.util.StringUtil;

import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.Properties;
import java.util.logging.Logger;

import static liquibase.ext.opensearch.database.OpenSearchLiquibaseDatabase.OPENSEARCH_PREFIX;

public class OpenSearchClientDriver implements Driver {
@Override
public Connection connect(final String url, final Properties info) {
//Not applicable for non JDBC DBs
throw new UnsupportedOperationException("Cannot initiate a SQL Connection for a NoSql DB");
}

public static boolean isOpenSearchURL(final String url) {
return StringUtil.trimToEmpty(url).startsWith(OPENSEARCH_PREFIX);
}

@Override
public boolean acceptsURL(final String url) {
return isOpenSearchURL(url);
}

@Override
public DriverPropertyInfo[] getPropertyInfo(final String url, final Properties info) throws SQLException {
return new DriverPropertyInfo[0];
}

@Override
public int getMajorVersion() {
return 0;
}

@Override
public int getMinorVersion() {
return 0;
}

@Override
public boolean jdbcCompliant() {
return false;
}

@Override
public Logger getParentLogger() {
return (Logger) Scope.getCurrentScope().getLog(getClass());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package liquibase.ext.opensearch.database;

import liquibase.exception.DatabaseException;
import liquibase.nosql.database.AbstractNoSqlConnection;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
import org.apache.hc.core5.reactor.ssl.TlsDetails;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.opensearch.client.json.jackson.JacksonJsonpMapper;
import org.opensearch.client.opensearch.OpenSearchClient;
import org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder;

import javax.net.ssl.SSLContext;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.sql.Driver;
import java.util.Optional;
import java.util.Properties;

import static liquibase.ext.opensearch.database.OpenSearchLiquibaseDatabase.OPENSEARCH_PREFIX;

@Getter
@Setter
@NoArgsConstructor
public class OpenSearchConnection extends AbstractNoSqlConnection {

// FIXME: protected due to tests using it
protected OpenSearchClient openSearchClient;

protected URI uri;
protected Properties connectionProperties;

@Override
public boolean supports(final String url) {
if (url == null) {
return false;
}
return url.toLowerCase().startsWith(OPENSEARCH_PREFIX);
}

@Override
public void open(final String url, final Driver driverObject, final Properties driverProperties) throws DatabaseException {
String realUrl = url;
if (realUrl.toLowerCase().startsWith(OPENSEARCH_PREFIX)) {
realUrl = realUrl.substring(OPENSEARCH_PREFIX.length());
}

this.connectionProperties = driverProperties;

try {
this.uri = new URI(realUrl);
this.connect(this.uri, driverProperties);
} catch (final Exception e) {
throw new DatabaseException("Could not open connection to database: " + realUrl);
}
}

@Override
public void close() throws DatabaseException {
this.openSearchClient = null;
this.connectionProperties = null;
this.uri = null;
}

@Override
public String getCatalog() throws DatabaseException {
return null; // OpenSearch doesn't have catalogs (called schemas in various RDBMS)
}

@Override
public String getDatabaseProductName() throws DatabaseException {
return OpenSearchLiquibaseDatabase.PRODUCT_NAME;
}

@Override
public String getURL() {
return this.uri.toString();
}

@Override
public String getConnectionUserName() {
return this.connectionProperties.getProperty("username");
}

@Override
public boolean isClosed() throws DatabaseException {
return this.openSearchClient == null;
}

private void connect(final URI uri, final Properties info) throws DatabaseException {
final HttpHost host = HttpHost.create(uri);

final var transport = ApacheHttpClient5TransportBuilder
.builder(host)
.setHttpClientConfigCallback(httpClientBuilder -> {
// TODO: support other credential providers
final var username = Optional.ofNullable(info.getProperty("user"));
final var password = Optional.ofNullable(info.getProperty("password"));

if (username.isPresent()) {
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(host),
new UsernamePasswordCredentials(username.get(), password.orElse("").toCharArray()));

httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
}

final SSLContext sslcontext;
try {
sslcontext = SSLContextBuilder
.create()
.loadTrustMaterial(null, (chains, authType) -> true)
.build();
} catch (final NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
throw new RuntimeException(e);
}

final TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create()
.setSslContext(sslcontext)
// disable the certificate since our testing cluster just uses the default security configuration
.setHostnameVerifier(NoopHostnameVerifier.INSTANCE)
// See https://issues.apache.org/jira/browse/HTTPCLIENT-2219
.setTlsDetailsFactory(sslEngine -> new TlsDetails(sslEngine.getSession(), sslEngine.getApplicationProtocol()))
.build();

final PoolingAsyncClientConnectionManager connectionManager = PoolingAsyncClientConnectionManagerBuilder.create()
.setTlsStrategy(tlsStrategy)
.build();

return httpClientBuilder
.setConnectionManager(connectionManager);
})
.setMapper(new JacksonJsonpMapper())
.build();

this.openSearchClient = new OpenSearchClient(transport);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package liquibase.ext.opensearch.database;

import liquibase.CatalogAndSchema;
import liquibase.exception.LiquibaseException;
import liquibase.nosql.database.AbstractNoSqlDatabase;
import lombok.NoArgsConstructor;

@NoArgsConstructor
public class OpenSearchLiquibaseDatabase extends AbstractNoSqlDatabase {
public static final String PRODUCT_NAME = "OpenSearch";
public static final String PRODUCT_SHORT_NAME = "opensearch";
public static final String OPENSEARCH_PREFIX = PRODUCT_SHORT_NAME + ":";

@Override
public void dropDatabaseObjects(final CatalogAndSchema schemaToDrop) throws LiquibaseException {
// TODO: implement
}

@Override
public String getDefaultDriver(final String url) {
if (OpenSearchClientDriver.isOpenSearchURL(url)) {
return OpenSearchClientDriver.class.getName();
}
return null;
}

@Override
public String getDatabaseProductName() {
return PRODUCT_NAME;
}

@Override
public String getShortName() {
return PRODUCT_SHORT_NAME;
}

@Override
public Integer getDefaultPort() {
return 9200;
}

@Override
protected String getDefaultDatabaseProductName() {
return PRODUCT_NAME;
}

@Override
public String getDatabaseChangeLogTableName() {
// OpenSearch only supports lowercase index names
return super.getDatabaseChangeLogTableName().toLowerCase();
}

@Override
public String getDatabaseChangeLogLockTableName() {
// OpenSearch only supports lowercase index names
return super.getDatabaseChangeLogLockTableName().toLowerCase();
}
}
Loading

0 comments on commit 7ea8322

Please sign in to comment.