From 6b25b56e10d0063d9c15b2c355607858eb460fbb Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Thu, 6 Jul 2023 16:07:57 +0300 Subject: [PATCH] RED-7094 - extracted liquibase code --- ...atabaseTenantCommonsAutoConfiguration.java | 3 +- .../TenantMessagingConfiguration.java | 2 +- .../providers/TenantCreatedListener.java | 154 ++-------------- .../providers/TenantLiquibaseInitializer.java | 167 ++++++++++++++++++ 4 files changed, 181 insertions(+), 145 deletions(-) create mode 100644 src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantLiquibaseInitializer.java diff --git a/src/main/java/com/knecon/fforesight/databasetenantcommons/DatabaseTenantCommonsAutoConfiguration.java b/src/main/java/com/knecon/fforesight/databasetenantcommons/DatabaseTenantCommonsAutoConfiguration.java index c1d1898..dddc32a 100644 --- a/src/main/java/com/knecon/fforesight/databasetenantcommons/DatabaseTenantCommonsAutoConfiguration.java +++ b/src/main/java/com/knecon/fforesight/databasetenantcommons/DatabaseTenantCommonsAutoConfiguration.java @@ -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 { } diff --git a/src/main/java/com/knecon/fforesight/databasetenantcommons/TenantMessagingConfiguration.java b/src/main/java/com/knecon/fforesight/databasetenantcommons/TenantMessagingConfiguration.java index 33dda89..6f52572 100644 --- a/src/main/java/com/knecon/fforesight/databasetenantcommons/TenantMessagingConfiguration.java +++ b/src/main/java/com/knecon/fforesight/databasetenantcommons/TenantMessagingConfiguration.java @@ -33,7 +33,7 @@ public class TenantMessagingConfiguration { @Bean public Queue persistenceServiceTenantDLQ() { - return QueueBuilder.durable(tenantCreatedEventQueue).build(); + return QueueBuilder.durable(tenantCreatedDLQ).build(); } diff --git a/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantCreatedListener.java b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantCreatedListener.java index a8db5c3..2ff99b2 100644 --- a/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantCreatedListener.java +++ b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantCreatedListener.java @@ -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 SUPPORTED_DATABASES = Set.of("postgresql"); - private static final Set 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) stmt -> stmt.execute("CREATE SCHEMA " + tenantRequest.getDatabaseConnection().getSchema())); - jdbcTemplate.execute((StatementCallback) 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()); } } diff --git a/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantLiquibaseInitializer.java b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantLiquibaseInitializer.java new file mode 100644 index 0000000..2110c09 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantLiquibaseInitializer.java @@ -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 SUPPORTED_DATABASES = Set.of("postgresql"); + private static final Set 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) stmt -> stmt.execute("CREATE SCHEMA " + tenantRequest.getDatabaseConnection().getSchema())); + jdbcTemplate.execute((StatementCallback) 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; + } + +}