From ee488a6cd07a49f6815f6adbaa62bc7cd5a45f73 Mon Sep 17 00:00:00 2001 From: Yaroslav_Kiriak Date: Thu, 8 Aug 2024 12:21:12 +0300 Subject: [PATCH] EUREKA-65: add script to migrate data from mod-consortia --- .../consortia/service/FolioTenantService.java | 40 +++- .../db/changelog/changelog-master.xml | 1 + .../migrate-data-from-mod-consortia.xml | 177 ++++++++++++++++++ .../java/org/folio/consortia/base/BaseIT.java | 2 +- .../consortia/base/BaseRepositoryTest.java | 2 +- .../repository/ConsortiaMigrationTest.java | 128 +++++++++++++ src/test/resources/sql/consortia_data.sql | 13 ++ 7 files changed, 357 insertions(+), 6 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/migrate-data-from-mod-consortia.xml create mode 100644 src/test/java/org/folio/consortia/repository/ConsortiaMigrationTest.java create mode 100644 src/test/resources/sql/consortia_data.sql diff --git a/src/main/java/org/folio/consortia/service/FolioTenantService.java b/src/main/java/org/folio/consortia/service/FolioTenantService.java index 0e873e04..4b84c1fc 100644 --- a/src/main/java/org/folio/consortia/service/FolioTenantService.java +++ b/src/main/java/org/folio/consortia/service/FolioTenantService.java @@ -1,6 +1,9 @@ package org.folio.consortia.service; import java.sql.ResultSet; +import java.util.Map; +import liquibase.exception.LiquibaseException; +import liquibase.exception.UnexpectedLiquibaseException; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @@ -9,6 +12,7 @@ import org.folio.consortia.domain.dto.CustomField; import org.folio.consortia.domain.dto.CustomFieldType; import org.folio.spring.FolioExecutionContext; +import org.folio.spring.exception.TenantUpgradeException; import org.folio.spring.liquibase.FolioSpringLiquibase; import org.folio.spring.service.PrepareSystemUserService; import org.folio.spring.service.SystemUserScopedExecutionService; @@ -26,7 +30,8 @@ @Primary public class FolioTenantService extends TenantService { - private static final String EXIST_SQL = "SELECT EXISTS(SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?)"; + private static final String EXIST_SQL = + "SELECT EXISTS(SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?)"; private final KafkaService kafkaService; private final CustomFieldService customFieldService; @@ -36,6 +41,7 @@ public class FolioTenantService extends TenantService { private final CustomFieldsRetryProperties customFieldsRetryProperties; private static final String ORIGINAL_TENANT_ID_NAME = "originalTenantId"; + private static final String TENANT_NAME_PARAMETER = "tenantname"; private static final CustomField ORIGINAL_TENANT_ID_CUSTOM_FIELD = CustomField.builder() .name(ORIGINAL_TENANT_ID_NAME) .entityType("user") @@ -62,6 +68,31 @@ public FolioTenantService(JdbcTemplate jdbcTemplate, this.customFieldsRetryProperties = customFieldsRetryProperties; } + /* + * Because of the liquibase.Scope implementation for the SpringLiquibase it is not possible to run several + * SpringLiquibase executions simultaneously. That is why this method must be synchronized. + */ + @Override + public synchronized void createOrUpdateTenant(TenantAttributes tenantAttributes) { + if (folioSpringLiquibase != null) { + var params = Map.of(TENANT_NAME_PARAMETER, folioExecutionContext.getTenantId()); + folioSpringLiquibase.setChangeLogParameters(params); + log.info("Set ChangeLog parameters: {}", params); + + folioSpringLiquibase.setDefaultSchema(getSchemaName()); + log.info("About to start liquibase update for tenant [{}]", context.getTenantId()); + + try { + folioSpringLiquibase.performLiquibaseUpdate(); + } catch (LiquibaseException | UnexpectedLiquibaseException e) { + throw new TenantUpgradeException(e); + } + log.info("Liquibase update for tenant [{}] executed successfully", context.getTenantId()); + } + + afterTenantUpdate(tenantAttributes); + } + @Override protected void afterTenantUpdate(TenantAttributes tenantAttributes) { try { @@ -75,8 +106,8 @@ protected void afterTenantUpdate(TenantAttributes tenantAttributes) { } /** - * Implemented by HSQLDB way - * Check if the tenant exists (by way of its database schema) + * Implemented by HSQLDB way. Check if the tenant exists (by way of its database schema) + * * @return if the tenant's database schema exists */ @Override @@ -92,7 +123,8 @@ protected boolean tenantExists() { private void createOriginalTenantIdCustomField() { systemUserScopedExecutionService.executeAsyncSystemUserScoped(folioExecutionContext.getTenantId(), () -> { if (ObjectUtils.isNotEmpty(customFieldService.getCustomFieldByName(ORIGINAL_TENANT_ID_NAME))) { - log.info("createOriginalTenantIdCustomField:: custom-field already available in tenant {} with name {}", folioExecutionContext.getTenantId(), ORIGINAL_TENANT_ID_NAME); + log.info("createOriginalTenantIdCustomField:: custom-field already available in tenant {} with name {}", + folioExecutionContext.getTenantId(), ORIGINAL_TENANT_ID_NAME); } else { createCustomFieldsWithRetry(); } diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml index 7d98f45f..75c03d2b 100644 --- a/src/main/resources/db/changelog/changelog-master.xml +++ b/src/main/resources/db/changelog/changelog-master.xml @@ -12,4 +12,5 @@ + diff --git a/src/main/resources/db/changelog/changes/migrate-data-from-mod-consortia.xml b/src/main/resources/db/changelog/changes/migrate-data-from-mod-consortia.xml new file mode 100644 index 00000000..62a9c489 --- /dev/null +++ b/src/main/resources/db/changelog/changes/migrate-data-from-mod-consortia.xml @@ -0,0 +1,177 @@ + + + + + + DO + ' + DECLARE + BEGIN + IF (EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = ''${tenantname}_mod_consortia'' AND table_name = ''consortia_configuration'')) + THEN + INSERT INTO ${tenantname}_mod_consortia_keycloak.consortia_configuration ( + id, central_tenant_id, created_by, created_date, updated_by, updated_date + ) SELECT + id, central_tenant_id, created_by, created_date, updated_by, + (CASE WHEN updated_date IS NULL THEN created_date ELSE updated_date END) as updated_date + FROM ${tenantname}_mod_consortia.consortia_configuration; + END IF; + END; + ' LANGUAGE plpgsql; + + + + + + DO + ' + DECLARE + BEGIN + IF (EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = ''${tenantname}_mod_consortia'' AND table_name = ''consortium'')) + THEN + INSERT INTO ${tenantname}_mod_consortia_keycloak.consortium ( + id, name, created_by, created_date, updated_by, updated_date + ) SELECT + id, name, created_by, created_date, updated_by, + (CASE WHEN updated_date IS NULL THEN created_date ELSE updated_date END) as updated_date + FROM ${tenantname}_mod_consortia.consortium; + END IF; + END; + ' LANGUAGE plpgsql; + + + + + + DO + ' + DECLARE + BEGIN + IF (EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = ''${tenantname}_mod_consortia'' AND table_name = ''pc_state'')) + THEN + INSERT INTO ${tenantname}_mod_consortia_keycloak.pc_state ( + id, status, total_records, created_by, created_date, updated_by, updated_date + ) SELECT + id, status, total_records, created_by, created_date, updated_by, + (CASE WHEN updated_date IS NULL THEN created_date ELSE updated_date END) as updated_date + FROM ${tenantname}_mod_consortia.pc_state; + END IF; + END; + ' LANGUAGE plpgsql; + + + + + + DO + ' + DECLARE + BEGIN + IF (EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = ''${tenantname}_mod_consortia'' AND table_name = ''pc_tenant_request'')) + THEN + INSERT INTO ${tenantname}_mod_consortia_keycloak.pc_tenant_request ( + id, pc_id, tenant_id, status, request_url, request_payload, response, response_status_code, + completed_date, created_by, created_date, updated_by, updated_date + ) SELECT + id, pc_id, tenant_id, status, request_url, request_payload, response, response_status_code, + completed_date, created_by, created_date, updated_by, + (CASE WHEN updated_date IS NULL THEN created_date ELSE updated_date END) as updated_date + FROM ${tenantname}_mod_consortia.pc_tenant_request; + END IF; + END; + ' LANGUAGE plpgsql; + + + + + + DO + ' + DECLARE + BEGIN + IF (EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = ''${tenantname}_mod_consortia'' AND table_name = ''sharing_instance'')) + THEN + INSERT INTO ${tenantname}_mod_consortia_keycloak.sharing_instance ( + id, instance_id, source_tenant_id, target_tenant_id, status, error, created_by, created_date, updated_by, updated_date + ) SELECT + id, instance_id, source_tenant_id, target_tenant_id, status, error, created_by, created_date, updated_by, + (CASE WHEN updated_date IS NULL THEN created_date ELSE updated_date END) as updated_date + FROM ${tenantname}_mod_consortia.sharing_instance; + END IF; + END; + ' LANGUAGE plpgsql; + + + + + + DO + ' + DECLARE + BEGIN + IF (EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = ''${tenantname}_mod_consortia'' AND table_name = ''sharing_setting'')) + THEN + INSERT INTO ${tenantname}_mod_consortia_keycloak.sharing_setting ( + id, setting_id, tenant_id, created_by, created_date, updated_by, updated_date + ) SELECT + id, setting_id, tenant_id, created_by, created_date, updated_by, + (CASE WHEN updated_date IS NULL THEN created_date ELSE updated_date END) as updated_date + FROM ${tenantname}_mod_consortia.sharing_setting; + END IF; + END; + ' LANGUAGE plpgsql; + + + + + + DO + ' + DECLARE + BEGIN + IF (EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = ''${tenantname}_mod_consortia'' AND table_name = ''tenant'')) + THEN + INSERT INTO ${tenantname}_mod_consortia_keycloak.tenant ( + id, name, consortium_id, code, is_central, created_by, created_date, updated_by, updated_date, setup_status, is_deleted + ) SELECT + id, name, consortium_id, code, is_central, created_by, created_date, updated_by, + (CASE WHEN updated_date IS NULL THEN created_date ELSE updated_date END) as updated_date, setup_status, is_deleted + FROM ${tenantname}_mod_consortia.tenant; + END IF; + END; + ' LANGUAGE plpgsql; + + + + + + DO + ' + DECLARE + BEGIN + IF (EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = ''${tenantname}_mod_consortia'' AND table_name = ''user_tenant'')) + THEN + INSERT INTO ${tenantname}_mod_consortia_keycloak.user_tenant ( + id, user_id, username, tenant_id, is_primary, created_by, created_date, updated_by, updated_date + ) SELECT + id, user_id, username, tenant_id, is_primary, created_by, created_date, updated_by, + (CASE WHEN updated_date IS NULL THEN created_date ELSE updated_date END) as updated_date + FROM ${tenantname}_mod_consortia.user_tenant; + END IF; + END; + ' LANGUAGE plpgsql; + + + + diff --git a/src/test/java/org/folio/consortia/base/BaseIT.java b/src/test/java/org/folio/consortia/base/BaseIT.java index 4de321b3..ce078aa7 100644 --- a/src/test/java/org/folio/consortia/base/BaseIT.java +++ b/src/test/java/org/folio/consortia/base/BaseIT.java @@ -53,7 +53,7 @@ public abstract class BaseIT { protected MockMvc mockMvc; protected static final int WIRE_MOCK_PORT = TestSocketUtils.findAvailableTcpPort(); - protected static final String TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkaWt1X2FkbWluIiwidXNlcl9pZCI6IjFkM2I1OGNiLTA3YjUtNWZjZC04YTJhLTNjZTA2YTBlYjkwZiIsImlhdCI6MTYxNjQyMDM5MywidGVuYW50IjoiZGlrdSJ9.2nvEYQBbJP1PewEgxixBWLHSX_eELiBEBpjufWiJZRs"; + public static final String TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkaWt1X2FkbWluIiwidXNlcl9pZCI6IjFkM2I1OGNiLTA3YjUtNWZjZC04YTJhLTNjZTA2YTBlYjkwZiIsImlhdCI6MTYxNjQyMDM5MywidGVuYW50IjoiZGlrdSJ9.2nvEYQBbJP1PewEgxixBWLHSX_eELiBEBpjufWiJZRs"; public static final String TENANT = "consortium"; protected static WireMockServer wireMockServer; protected static PostgreSQLContainer postgreDBContainer = new PostgreSQLContainer<>("postgres:12-alpine"); diff --git a/src/test/java/org/folio/consortia/base/BaseRepositoryTest.java b/src/test/java/org/folio/consortia/base/BaseRepositoryTest.java index 5d4eb9a3..215d1af7 100644 --- a/src/test/java/org/folio/consortia/base/BaseRepositoryTest.java +++ b/src/test/java/org/folio/consortia/base/BaseRepositoryTest.java @@ -15,7 +15,7 @@ import org.springframework.test.annotation.DirtiesContext; @Log4j2 -@DataJpaTest +@DataJpaTest(properties = "spring.liquibase.parameters.tenantname=consortium") @EnablePostgresExtension @DirtiesContext(classMode = AFTER_CLASS) @Import({FolioAuditorAware.class}) diff --git a/src/test/java/org/folio/consortia/repository/ConsortiaMigrationTest.java b/src/test/java/org/folio/consortia/repository/ConsortiaMigrationTest.java new file mode 100644 index 00000000..d41504e2 --- /dev/null +++ b/src/test/java/org/folio/consortia/repository/ConsortiaMigrationTest.java @@ -0,0 +1,128 @@ +package org.folio.consortia.repository; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.consortia.base.BaseIT.TENANT; +import static org.folio.consortia.base.BaseIT.TOKEN; +import static org.folio.consortia.base.BaseIT.asJsonString; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.common.ClasspathFileSource; +import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer; +import com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine; +import java.util.ArrayList; +import java.util.List; +import lombok.SneakyThrows; +import org.folio.consortia.repository.ConsortiaMigrationTest.DockerPostgresDataSourceInitializer; +import org.folio.consortia.support.extension.EnableKafkaExtension; +import org.folio.spring.integration.XOkapiHeaders; +import org.folio.tenant.domain.dto.TenantAttributes; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.HttpHeaders; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.springframework.test.util.TestSocketUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Testcontainers; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ContextConfiguration(initializers = DockerPostgresDataSourceInitializer.class) +@AutoConfigureMockMvc +@Testcontainers +@EmbeddedKafka +@EnableKafkaExtension +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) +class ConsortiaMigrationTest { + + @Autowired + protected MockMvc mockMvc; + @Autowired + private JdbcTemplate jdbcTemplate; + + protected static final int WIRE_MOCK_PORT = TestSocketUtils.findAvailableTcpPort(); + protected static WireMockServer wireMockServer; + protected static PostgreSQLContainer postgreDBContainer = new PostgreSQLContainer<>("postgres:12-alpine"); + + static { + postgreDBContainer.start(); + } + + @BeforeAll + static void beforeAll() { + wireMockServer = new WireMockServer(wireMockConfig().port(WIRE_MOCK_PORT) + .extensions(new ResponseTemplateTransformer( + TemplateEngine.defaultTemplateEngine(), true, new ClasspathFileSource("/"), new ArrayList<>()))); + wireMockServer.start(); + } + + @Test + @Sql("classpath:/sql/consortia_data.sql") + void create_positive_migrateConsortiaData() { + setUpTenant(mockMvc); + + var ids = jdbcTemplate.queryForList( + format("SELECT id FROM %s.%s", TENANT + "_mod_consortia_keycloak", "consortia_configuration"), String.class); + assertThat(ids.size()).isEqualTo(1); + assertThat(ids.iterator().next()).isEqualTo("e2628d7d-059a-46a1-a5ea-10a5a37b1af2"); + } + + @AfterAll + static void tearDown() { + wireMockServer.stop(); + } + + @SneakyThrows + static void setUpTenant(MockMvc mockMvc) { + mockMvc.perform(post("/_/tenant").content(asJsonString(new TenantAttributes().moduleTo("mod-consortia-keycloak"))) + .headers(defaultHeaders()) + .contentType(APPLICATION_JSON)).andExpect(status().isNoContent()); + } + + static HttpHeaders defaultHeaders() { + var httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(APPLICATION_JSON); + httpHeaders.put(XOkapiHeaders.TENANT, List.of(TENANT)); + httpHeaders.add(XOkapiHeaders.URL, wireMockServer.baseUrl()); + httpHeaders.add(XOkapiHeaders.TOKEN, TOKEN); + httpHeaders.add(XOkapiHeaders.USER_ID, "7698e46-c3e3-11ed-afa1-0242ac120002"); + return httpHeaders; + } + + static class DockerPostgresDataSourceInitializer implements + ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext context) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, + "spring.datasource.url=" + postgreDBContainer.getJdbcUrl(), + "spring.datasource.username=" + postgreDBContainer.getUsername(), + "spring.datasource.password=" + postgreDBContainer.getPassword()); + } + } + + @DynamicPropertySource + static void setFolioOkapiUrl(DynamicPropertyRegistry registry) { + registry.add("folio.okapi-url", () -> "http://localhost:" + WIRE_MOCK_PORT); + } +} diff --git a/src/test/resources/sql/consortia_data.sql b/src/test/resources/sql/consortia_data.sql new file mode 100644 index 00000000..15c88d72 --- /dev/null +++ b/src/test/resources/sql/consortia_data.sql @@ -0,0 +1,13 @@ +CREATE SCHEMA if NOT EXISTS consortium_mod_consortia; + +CREATE TABLE consortium_mod_consortia.consortia_configuration ( + id UUID PRIMARY KEY, + central_tenant_id TEXT NOT NULL UNIQUE, + created_by UUID, + created_date TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, + updated_by UUID, + updated_date TIMESTAMP WITHOUT TIME ZONE +); + +INSERT INTO consortium_mod_consortia.consortia_configuration (id, central_tenant_id) +VALUES ('e2628d7d-059a-46a1-a5ea-10a5a37b1af2', 'text');