RED-7094 - extracted liquibase code

This commit is contained in:
Timo Bejan 2023-07-06 16:07:57 +03:00
parent bd153595d0
commit 6b25b56e10
4 changed files with 181 additions and 145 deletions

View File

@ -3,6 +3,7 @@ package com.knecon.fforesight.databasetenantcommons;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
@ -13,7 +14,7 @@ import com.knecon.fforesight.tenantcommons.MultiTenancyAutoConfiguration;
@AutoConfiguration
@AutoConfigureAfter(MultiTenancyAutoConfiguration.class)
@ImportAutoConfiguration(MultiTenancyAutoConfiguration.class)
@EnableConfigurationProperties(TenantHikariSettings.class)
@EnableConfigurationProperties({TenantHikariSettings.class, LiquibaseProperties.class})
public class DatabaseTenantCommonsAutoConfiguration {
}

View File

@ -33,7 +33,7 @@ public class TenantMessagingConfiguration {
@Bean
public Queue persistenceServiceTenantDLQ() {
return QueueBuilder.durable(tenantCreatedEventQueue).build();
return QueueBuilder.durable(tenantCreatedDLQ).build();
}

View File

@ -1,71 +1,31 @@
package com.knecon.fforesight.databasetenantcommons.providers;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Set;
import javax.sql.DataSource;
import org.postgresql.util.PSQLException;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.StatementCallback;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.knecon.fforesight.databasetenantcommons.providers.events.TenantCreatedEvent;
import com.knecon.fforesight.databasetenantcommons.providers.utils.JDBCUtils;
import com.knecon.fforesight.tenantcommons.EncryptionDecryptionService;
import com.knecon.fforesight.tenantcommons.TenantProvider;
import com.knecon.fforesight.tenantcommons.model.TenantResponse;
import liquibase.exception.LiquibaseException;
import liquibase.integration.spring.SpringLiquibase;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@EnableConfigurationProperties(LiquibaseProperties.class)
@RequiredArgsConstructor
public class TenantCreatedListener {
private static final Set<String> SUPPORTED_DATABASES = Set.of("postgresql");
private static final Set<String> SQL_CONNECTION_ERROR_CODES = Set.of(
// connection_exception
"08000",
// connection_does_not_exist
"08003",
// connection_failure
"08006",
// invalid_catalog_name
"3D000");
private final TenantLiquibaseInitializer tenantLiquibaseInitializer;
private final LiquibaseProperties liquibaseProperties;
private final ResourceLoader resourceLoader;
private final MigrationService migrationService;
private final TenantProvider tenantProvider;
private final EncryptionDecryptionService encryptionDecryptionService;
@Value("${fforesight.multitenancy.tenant-created-queue:tenant-created}")
private String tenantCreatedQueue;
public TenantCreatedListener(@Qualifier("tenantLiquibaseProperties") LiquibaseProperties liquibaseProperties,
ResourceLoader resourceLoader,
EncryptionDecryptionService encryptionDecryptionService,
@Autowired(required = false) MigrationService migrationService,
TenantProvider tenantProvider) {
@PostConstruct
public void postConstruct() {
this.liquibaseProperties = liquibaseProperties;
this.resourceLoader = resourceLoader;
this.encryptionDecryptionService = encryptionDecryptionService;
this.migrationService = migrationService;
this.tenantProvider = tenantProvider;
log.info("Listener for tenant-created started for queue: {}", tenantCreatedQueue);
}
@ -73,99 +33,7 @@ public class TenantCreatedListener {
@RabbitListener(queues = "${fforesight.multitenancy.tenant-created-queue:tenant-created}")
public void createTenant(TenantCreatedEvent tenantRequest) {
var tenant = tenantProvider.getTenant(tenantRequest.getTenantId());
createSchema(tenant);
var jdbcUrl = JDBCUtils.buildJdbcUrlWithSchema(tenant.getDatabaseConnection());
validateJdbcUrl(jdbcUrl);
try (Connection connection = DriverManager.getConnection(jdbcUrl,
tenant.getDatabaseConnection().getUsername(),
encryptionDecryptionService.decrypt(tenant.getDatabaseConnection().getPassword()))) {
DataSource tenantDataSource = new SingleConnectionDataSource(connection, false);
runLiquibase(tenantDataSource);
} catch (PSQLException e) {
handleClientException(e);
handleInternalException(e);
}
if (migrationService != null) {
migrationService.runForTenant(tenantRequest.getTenantId());
}
}
private void createSchema(TenantResponse tenantRequest) {
var jdbcUrl = JDBCUtils.buildJdbcUrl(tenantRequest.getDatabaseConnection());
try (Connection connection = DriverManager.getConnection(jdbcUrl,
tenantRequest.getDatabaseConnection().getUsername(),
encryptionDecryptionService.decrypt(tenantRequest.getDatabaseConnection().getPassword()))) {
DataSource tenantDataSource = new SingleConnectionDataSource(connection, false);
JdbcTemplate jdbcTemplate = new JdbcTemplate(tenantDataSource);
jdbcTemplate.execute((StatementCallback<Boolean>) stmt -> stmt.execute("CREATE SCHEMA " + tenantRequest.getDatabaseConnection().getSchema()));
jdbcTemplate.execute((StatementCallback<Boolean>) stmt -> stmt.execute("GRANT USAGE ON SCHEMA " + tenantRequest.getDatabaseConnection()
.getSchema() + " TO " + tenantRequest.getDatabaseConnection().getUsername()));
} catch (Exception e) {
log.info("Could not create schema, ignoring");
}
}
@SneakyThrows
private void validateJdbcUrl(String jdbcUrl) {
try {
// just create a URI object to check if the string is a valid URI
var uri = new URI(jdbcUrl);
var subUri = new URI(uri.getSchemeSpecificPart());
if (uri.getScheme() == null || subUri.getScheme() == null || !uri.getScheme().equals("jdbc") || !SUPPORTED_DATABASES.contains(subUri.getScheme())) {
throw new IllegalArgumentException("Your jdbcUrl is not valid.");
}
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Your jdbcUrl is not valid.", e);
}
}
private void runLiquibase(DataSource dataSource) throws LiquibaseException {
SpringLiquibase liquibase = getSpringLiquibase(dataSource);
liquibase.afterPropertiesSet();
}
private void handleClientException(PSQLException e) {
if (e.getSQLState().equals("28000") || e.getSQLState().equals("28P01")) {
throw new IllegalArgumentException("Database credentials are not correct. Please check them.");
}
if (SQL_CONNECTION_ERROR_CODES.contains(e.getSQLState())) {
throw new IllegalArgumentException("Error when connecting to tenant database. Please check the jdbcUrl parameter.");
}
}
private void handleInternalException(PSQLException e) {
log.error(String.format("Connection to tenant DB failed with SQL state %s. Please check if the tenant DB is still running. " + //
"If yes please check the connection configuration.", e.getSQLState()), e);
throw new RuntimeException("Could not connect to the tenant DB. This is an internal error.", e);
}
protected SpringLiquibase getSpringLiquibase(DataSource dataSource) {
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setResourceLoader(resourceLoader);
liquibase.setDataSource(dataSource);
liquibase.setChangeLog(liquibaseProperties.getChangeLog());
liquibase.setContexts(liquibaseProperties.getContexts());
return liquibase;
tenantLiquibaseInitializer.initializeTenant(tenantRequest.getTenantId());
}
}

View File

@ -0,0 +1,167 @@
package com.knecon.fforesight.databasetenantcommons.providers;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Set;
import javax.sql.DataSource;
import org.postgresql.util.PSQLException;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.StatementCallback;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import org.springframework.stereotype.Service;
import com.knecon.fforesight.databasetenantcommons.providers.events.TenantCreatedEvent;
import com.knecon.fforesight.databasetenantcommons.providers.utils.JDBCUtils;
import com.knecon.fforesight.tenantcommons.EncryptionDecryptionService;
import com.knecon.fforesight.tenantcommons.TenantProvider;
import com.knecon.fforesight.tenantcommons.model.TenantResponse;
import liquibase.exception.LiquibaseException;
import liquibase.integration.spring.SpringLiquibase;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class TenantLiquibaseInitializer {
private static final Set<String> SUPPORTED_DATABASES = Set.of("postgresql");
private static final Set<String> SQL_CONNECTION_ERROR_CODES = Set.of(
// connection_exception
"08000",
// connection_does_not_exist
"08003",
// connection_failure
"08006",
// invalid_catalog_name
"3D000");
private final LiquibaseProperties liquibaseProperties;
private final ResourceLoader resourceLoader;
private final MigrationService migrationService;
private final TenantProvider tenantProvider;
private final EncryptionDecryptionService encryptionDecryptionService;
public TenantLiquibaseInitializer(@Qualifier("tenantLiquibaseProperties") LiquibaseProperties liquibaseProperties,
ResourceLoader resourceLoader,
EncryptionDecryptionService encryptionDecryptionService,
@Autowired(required = false) MigrationService migrationService,
TenantProvider tenantProvider) {
this.liquibaseProperties = liquibaseProperties;
this.resourceLoader = resourceLoader;
this.encryptionDecryptionService = encryptionDecryptionService;
this.migrationService = migrationService;
this.tenantProvider = tenantProvider;
}
@SneakyThrows
public void initializeTenant(String tenantId) {
var tenant = tenantProvider.getTenant(tenantId);
createSchema(tenant);
var jdbcUrl = JDBCUtils.buildJdbcUrlWithSchema(tenant.getDatabaseConnection());
validateJdbcUrl(jdbcUrl);
try (Connection connection = DriverManager.getConnection(jdbcUrl,
tenant.getDatabaseConnection().getUsername(),
encryptionDecryptionService.decrypt(tenant.getDatabaseConnection().getPassword()))) {
DataSource tenantDataSource = new SingleConnectionDataSource(connection, false);
runLiquibase(tenantDataSource);
} catch (PSQLException e) {
handleClientException(e);
handleInternalException(e);
}
if (migrationService != null) {
migrationService.runForTenant(tenantId);
}
}
private void createSchema(TenantResponse tenantRequest) {
var jdbcUrl = JDBCUtils.buildJdbcUrl(tenantRequest.getDatabaseConnection());
try (Connection connection = DriverManager.getConnection(jdbcUrl,
tenantRequest.getDatabaseConnection().getUsername(),
encryptionDecryptionService.decrypt(tenantRequest.getDatabaseConnection().getPassword()))) {
DataSource tenantDataSource = new SingleConnectionDataSource(connection, false);
JdbcTemplate jdbcTemplate = new JdbcTemplate(tenantDataSource);
jdbcTemplate.execute((StatementCallback<Boolean>) stmt -> stmt.execute("CREATE SCHEMA " + tenantRequest.getDatabaseConnection().getSchema()));
jdbcTemplate.execute((StatementCallback<Boolean>) stmt -> stmt.execute("GRANT USAGE ON SCHEMA " + tenantRequest.getDatabaseConnection()
.getSchema() + " TO " + tenantRequest.getDatabaseConnection().getUsername()));
} catch (Exception e) {
log.info("Could not create schema, ignoring");
}
}
@SneakyThrows
private void validateJdbcUrl(String jdbcUrl) {
try {
// just create a URI object to check if the string is a valid URI
var uri = new URI(jdbcUrl);
var subUri = new URI(uri.getSchemeSpecificPart());
if (uri.getScheme() == null || subUri.getScheme() == null || !uri.getScheme().equals("jdbc") || !SUPPORTED_DATABASES.contains(subUri.getScheme())) {
throw new IllegalArgumentException("Your jdbcUrl is not valid.");
}
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Your jdbcUrl is not valid.", e);
}
}
private void runLiquibase(DataSource dataSource) throws LiquibaseException {
SpringLiquibase liquibase = getSpringLiquibase(dataSource);
liquibase.afterPropertiesSet();
}
private void handleClientException(PSQLException e) {
if (e.getSQLState().equals("28000") || e.getSQLState().equals("28P01")) {
throw new IllegalArgumentException("Database credentials are not correct. Please check them.");
}
if (SQL_CONNECTION_ERROR_CODES.contains(e.getSQLState())) {
throw new IllegalArgumentException("Error when connecting to tenant database. Please check the jdbcUrl parameter.");
}
}
private void handleInternalException(PSQLException e) {
log.error(String.format("Connection to tenant DB failed with SQL state %s. Please check if the tenant DB is still running. " + //
"If yes please check the connection configuration.", e.getSQLState()), e);
throw new RuntimeException("Could not connect to the tenant DB. This is an internal error.", e);
}
protected SpringLiquibase getSpringLiquibase(DataSource dataSource) {
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setResourceLoader(resourceLoader);
liquibase.setDataSource(dataSource);
liquibase.setChangeLog(liquibaseProperties.getChangeLog());
liquibase.setContexts(liquibaseProperties.getContexts());
return liquibase;
}
}