diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/AllITs.java index b072a31fd..76c1ad054 100644 --- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java +++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java @@ -18,6 +18,7 @@ import org.apache.unomi.itests.migration.Migrate16xToCurrentVersionIT; import org.apache.unomi.itests.graphql.*; +import org.apache.unomi.itests.migration.Migrate30ToCurrentVersionIT; import org.apache.unomi.itests.migration.Migrate40IndexMappingIT; import org.apache.unomi.itests.migration.Migrate40TenantAssignmentIT; import org.apache.unomi.itests.migration.MigrationIT; @@ -34,6 +35,7 @@ */ @RunWith(ProgressSuite.class) @SuiteClasses({ + Migrate30ToCurrentVersionIT.class, Migrate16xToCurrentVersionIT.class, MigrationIT.class, Migrate40IndexMappingIT.class, diff --git a/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java b/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java index 6f3c510ef..38d88b17a 100644 --- a/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java +++ b/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java @@ -18,6 +18,7 @@ import org.apache.unomi.itests.migration.Migrate16xToCurrentVersionIT; import org.apache.unomi.itests.graphql.*; +import org.apache.unomi.itests.migration.Migrate30ToCurrentVersionIT; import org.apache.unomi.itests.migration.Migrate40IndexMappingIT; import org.apache.unomi.itests.migration.Migrate40TenantAssignmentIT; import org.apache.unomi.itests.migration.MigrationIT; @@ -35,6 +36,7 @@ */ @RunWith(ProgressSuite.class) @SuiteClasses({ + Migrate30ToCurrentVersionIT.class, Migrate16xToCurrentVersionIT.class, MigrationIT.class, Migrate40IndexMappingIT.class, diff --git a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java index 63e0755ef..aa325edf4 100644 --- a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java @@ -40,6 +40,8 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.*; @Category(SearchBackendIT.class) @@ -125,6 +127,15 @@ public void waitForStartup() throws InterruptedException { // Restore snapshot from 1.6.x try (CloseableHttpClient httpClient = createSearchEngineHttpClient()) { + // A prior migration IT (e.g. Migrate30) may have left open context-* indices. + // Snapshot restore refuses to overwrite open indices, so clear them first. + for (String index : MigrationUtils.getIndexesPrefixedBy(httpClient, getEsBaseUrl(), INDEX_PREFIX_CONTEXT)) { + LOGGER.info("Deleting existing index before 1.6 snapshot restore: {}", index); + MigrationUtils.deleteIndex(httpClient, getEsBaseUrl(), index); + } + // Its completed history would make this migration skip every 4.0.0 step. + Files.deleteIfExists(Paths.get(karafData(), "migration", "history.json")); + // Create snapshot repo HttpUtils.executePutRequest(httpClient, getEsSnapshotRepo(), resourceAsString(RESOURCE_CREATE_SNAPSHOTS_REPO), null); // Get snapshot, insure it exists diff --git a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java new file mode 100644 index 000000000..6137b73a6 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java @@ -0,0 +1,375 @@ +/* + * 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.unomi.itests.migration; + +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.unomi.api.conditions.ConditionType; +import org.apache.unomi.api.tenants.Tenant; +import org.apache.unomi.itests.BaseIT; +import org.apache.unomi.itests.persistence.SearchBackendIT; +import org.apache.unomi.shell.migration.utils.HttpRequestException; +import org.apache.unomi.shell.migration.utils.HttpUtils; +import org.apache.unomi.shell.migration.utils.MigrationUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +/** + * UNOMI-998: migrate a synthetic Unomi 3.0 data set with {@code unomi:migrate 3.0.0} + * before Unomi starts, then assert the post-migration shape that 996/997 fixed. + *

+ * Must run before {@link Migrate16xToCurrentVersionIT}, which deletes this fixture and the + * migration history before restoring its 1.6 snapshot. Where that snapshot migration runs + * (Elasticsearch), this test leaves Unomi stopped and only checks the migrated indices. Uses HTTP seeding so both Elasticsearch and OpenSearch + * can run it ({@code httpAdminApi}), unlike the 1.6 snapshot path. + *

+ * One {@code @Test} method only: {@code @Before} must not re-seed on every method, or the + * second migrate is skipped by migration history and leaves documents without tenants. + *

+ * Pax Exam still installs the full feature set for the shared suite. The boot-feature + * mapping path is guarded by asserting that the migration command bundle itself ships + * the engine mappings (UNOMI-996). + */ +@Category(SearchBackendIT.class) +public class Migrate30ToCurrentVersionIT extends BaseIT { + + private static final Logger LOGGER = LoggerFactory.getLogger(Migrate30ToCurrentVersionIT.class); + + private static final String MIGRATION_COMMAND = "unomi:migrate 3.0.0 true"; + private static final long MIGRATION_TIMEOUT = 900000L; + + private static final String INDEX_PREFIX = "context-"; + private static final String INDEX_SYSTEMITEMS = INDEX_PREFIX + "systemitems"; + private static final String INDEX_PROFILE = INDEX_PREFIX + "profile"; + private static final String INDEX_GEONAME = INDEX_PREFIX + "geonameentry"; + private static final String INDEX_CLUSTERNODE = INDEX_PREFIX + "clusternode"; + private static final String INDEX_SFDC = INDEX_PREFIX + "sfdcconfiguration"; + private static final String INDEX_TENANT = INDEX_PREFIX + "tenant"; + + private static final String USER_RULE_ID = "unomi998-user-rule"; + private static final String USER_SEGMENT_ID = "unomi998-user-segment"; + private static final String GEO_ID = "unomi998-paris"; + private static final String NODE_ID = "unomi998-node"; + private static final String SFDC_ID = "unomi998-sfdc"; + private static final String PROFILE_ID = "unomi998-profile"; + private static final String OLD_CONDITION_MARKER = "unomi998-3.0-seed-booleanCondition"; + // Post-2.2 document id for system condition types: system__conditiontype + private static final String SHIPPED_CONDITION_DOC_ID = "system_booleanCondition_conditiontype"; + + private static final String SIMPLE_MAPPING = "{" + + "\"settings\":{\"number_of_shards\":1,\"number_of_replicas\":0}," + + "\"mappings\":{\"properties\":{" + + "\"itemId\":{\"type\":\"keyword\"}," + + "\"itemType\":{\"type\":\"text\",\"fields\":{\"keyword\":{\"type\":\"keyword\"}}}," + + "\"scope\":{\"type\":\"keyword\"}," + + "\"tenantId\":{\"type\":\"keyword\"}," + + "\"metadata\":{\"type\":\"object\",\"enabled\":true}," + + "\"name\":{\"type\":\"keyword\"}," + + "\"properties\":{\"type\":\"object\",\"enabled\":true}" + + "}}}"; + + private CloseableHttpClient httpClient; + + @Override + @Before + public void waitForStartup() throws InterruptedException { + checkSearchEngine(); + + if (!persistenceCapabilities().httpAdminApi()) { + System.out.println("httpAdminApi not supported for provider " + + getPersistenceBackend().providerId() + " — starting Unomi without 3.0 migrate seed"); + super.waitForStartup(); + return; + } + + try { + httpClient = createSearchEngineHttpClient(); + // A completed history from an earlier run would make this migrate skip every step + Path history = Paths.get(karafData(), "migration", "history.json"); + Files.deleteIfExists(history); + seedUnomi30Dataset(httpClient); + assertMigrationBundleShipsEngineMappings(); + + System.out.println("Launching migration from 3.0.0..."); + LOGGER.info("Launching migration from 3.0.0..."); + String migrationOutput = executeCommand(MIGRATION_COMMAND, MIGRATION_TIMEOUT, false); + System.out.println("Migration command output results:"); + System.out.println(migrationOutput); + Assert.assertNotNull(migrationOutput); + Assert.assertTrue("migration must run 4.0.0 tenant scripts", + migrationOutput.contains("tenantDocumentIds")); + // Only written once every script has finished without error + Assert.assertEquals("migration must finish", "COMPLETED", + getObjectMapper().readTree(history.toFile()).path("migrationStatus").asText()); + + assertMigrationWroteSystemDefinitionAndTenant(); + prepareSearchEngineAfterMigration(); + } catch (Throwable t) { + LOGGER.error("Error during 3.0 migration setup", t); + throw new RuntimeException("Error during 3.0 migration setup", t); + } + + // Where the 1.6 snapshot migration runs next, it needs Unomi still stopped: started here, + // Unomi would keep running on indices that test deletes and never redeploy its definitions. + if (!persistenceCapabilities().snapshotRestoreMigration()) { + super.waitForStartup(); + } + } + + @After + public void closeHttpClient() throws Exception { + if (httpClient != null) { + httpClient.close(); + } + } + + @Test + public void checkMigratedDataFrom30() throws Exception { + Assume.assumeTrue( + "HTTP admin API required (provider=" + getPersistenceBackend().providerId() + ")", + persistenceCapabilities().httpAdminApi()); + + assertUserContentStaysOutOfSystemTenant(); + assertGeonamesClusterNodeAndSfdc(); + assertEverySeededDocumentHasATenant(); + assertApiKeyFileNamesCorrectHeaders(); + if (unomiStarted) { + assertDefaultTenantExists(); + assertShippedDefinitionsAreFourZero(); + } + } + + /** + * Checked before Unomi starts: startup deploys the bundled definitions and BaseIT creates the + * test tenant when it is missing, so afterwards both would exist whatever the migration did. + */ + private void assertMigrationWroteSystemDefinitionAndTenant() throws Exception { + HttpUtils.executePostRequest(httpClient, getSearchEngineBaseUrl() + "/_refresh", null, null); + + JsonNode shipped = getByDocumentId(INDEX_SYSTEMITEMS, SHIPPED_CONDITION_DOC_ID); + Assert.assertNotNull("migration must write " + SHIPPED_CONDITION_DOC_ID, shipped); + Assert.assertEquals("shipped definitions go to system", "system", + shipped.path("_source").path("tenantId").asText()); + + JsonNode tenants = getObjectMapper().readTree(HttpUtils.executeGetRequest(httpClient, + getSearchEngineBaseUrl() + "/" + INDEX_TENANT + "/_search", null)).path("hits").path("hits"); + Assert.assertEquals("migration must write the default tenant", 1, tenants.size()); + Assert.assertEquals(TEST_TENANT_ID, tenants.get(0).path("_source").path("itemId").asText()); + } + + private void assertDefaultTenantExists() throws Exception { + Assert.assertTrue("tenant index must exist", + MigrationUtils.indexExists(httpClient, getSearchEngineBaseUrl(), INDEX_TENANT)); + + Tenant tenant = tenantService.getTenant(TEST_TENANT_ID); + Assert.assertNotNull("default tenant must exist after migration (UNOMI-998)", tenant); + Assert.assertEquals(TEST_TENANT_ID, tenant.getItemId()); + Assert.assertNotNull("tenant must have a public API key", tenant.getPublicApiKey()); + Assert.assertNotNull("tenant must have a private API key", tenant.getPrivateApiKey()); + } + + private void assertUserContentStaysOutOfSystemTenant() throws Exception { + JsonNode rule = findByItemId(INDEX_SYSTEMITEMS, USER_RULE_ID); + Assert.assertNotNull("user rule must still exist", rule); + Assert.assertEquals(TEST_TENANT_ID, rule.path("_source").path("tenantId").asText()); + Assert.assertTrue(rule.path("_id").asText().startsWith(TEST_TENANT_ID + "_")); + + JsonNode segment = findByItemId(INDEX_SYSTEMITEMS, USER_SEGMENT_ID); + Assert.assertNotNull("user segment must still exist", segment); + Assert.assertEquals(TEST_TENANT_ID, segment.path("_source").path("tenantId").asText()); + Assert.assertFalse("user segment must not use the system document id", + segment.path("_id").asText().startsWith("system_")); + } + + private void assertGeonamesClusterNodeAndSfdc() throws Exception { + JsonNode geo = findByItemId(INDEX_GEONAME, GEO_ID); + Assert.assertNotNull(geo); + Assert.assertEquals("system", geo.path("_source").path("tenantId").asText()); + + JsonNode node = findByItemId(INDEX_CLUSTERNODE, NODE_ID); + Assert.assertNotNull(node); + Assert.assertEquals("cluster nodes stay in the configured tenant", TEST_TENANT_ID, + node.path("_source").path("tenantId").asText()); + + // Unknown index type uses forceDefaultTenant — literal scope "system" does not win + JsonNode sfdc = findByItemId(INDEX_SFDC, SFDC_ID); + Assert.assertNotNull(sfdc); + Assert.assertEquals(TEST_TENANT_ID, sfdc.path("_source").path("tenantId").asText()); + + JsonNode profile = findByItemId(INDEX_PROFILE, PROFILE_ID); + Assert.assertNotNull(profile); + Assert.assertEquals(TEST_TENANT_ID, profile.path("_source").path("tenantId").asText()); + } + + private void assertEverySeededDocumentHasATenant() throws Exception { + for (String index : Arrays.asList(INDEX_SYSTEMITEMS, INDEX_PROFILE, INDEX_GEONAME, INDEX_CLUSTERNODE, INDEX_SFDC, INDEX_TENANT)) { + if (!MigrationUtils.indexExists(httpClient, getSearchEngineBaseUrl(), index)) { + continue; + } + JsonNode hits = getObjectMapper().readTree(HttpUtils.executePostRequest(httpClient, + getSearchEngineBaseUrl() + "/" + index + "/_search", + "{\"size\":10,\"_source\":false,\"query\":{\"bool\":{\"must_not\":{\"exists\":{\"field\":\"tenantId\"}}}}}", null)) + .path("hits").path("hits"); + Assert.assertEquals(index + " has documents without tenantId: " + hits, 0, hits.size()); + } + } + + private void assertShippedDefinitionsAreFourZero() throws Exception { + ConditionType booleanCondition = definitionsService.getConditionType("booleanCondition"); + Assert.assertNotNull("booleanCondition must be available after migration", booleanCondition); + String description = booleanCondition.getMetadata() != null + ? booleanCondition.getMetadata().getDescription() : null; + Assert.assertNotEquals("3.0 seeded marker must not win over the 4.0 bundle definition", + OLD_CONDITION_MARKER, description); + } + + private void assertApiKeyFileNamesCorrectHeaders() throws Exception { + Path secretsDir = Paths.get(karafData(), "migration", "secrets"); + Assert.assertTrue("migration secrets directory must exist: " + secretsDir, Files.isDirectory(secretsDir)); + + Path keyFile = null; + try (DirectoryStream stream = Files.newDirectoryStream(secretsDir, "tenant-api-keys-" + TEST_TENANT_ID + "-*.txt")) { + for (Path path : stream) { + keyFile = path; + break; + } + } + Assert.assertNotNull("migration must write a tenant API key file", keyFile); + + String content = Files.readString(keyFile, StandardCharsets.UTF_8); + Assert.assertTrue("public key must name X-Unomi-Api-Key\n" + content, + content.contains("X-Unomi-Api-Key")); + Assert.assertTrue("private key must mention Basic authentication\n" + content, + content.toLowerCase().contains("basic")); + Assert.assertFalse("must not advertise the non-existent X-Unomi-Public-Key header\n" + content, + content.contains("X-Unomi-Public-Key")); + Assert.assertFalse("must not advertise the non-existent X-Unomi-Key header\n" + content, + content.contains("X-Unomi-Key header")); + } + + private void seedUnomi30Dataset(CloseableHttpClient client) throws Exception { + String es = getSearchEngineBaseUrl(); + System.out.println("Seeding synthetic Unomi 3.0 dataset..."); + LOGGER.info("Seeding synthetic Unomi 3.0 dataset..."); + + // The migration only creates the tenant (and its key file) when the tenant index is missing + MigrationUtils.deleteIndex(client, es, INDEX_TENANT); + for (String index : Arrays.asList(INDEX_SYSTEMITEMS, INDEX_PROFILE, INDEX_GEONAME, INDEX_CLUSTERNODE, INDEX_SFDC)) { + MigrationUtils.deleteIndex(client, es, index); + MigrationUtils.createIndex(client, es, index, SIMPLE_MAPPING); + } + + putDoc(client, INDEX_SYSTEMITEMS, "booleanCondition", + "{\"itemId\":\"booleanCondition\",\"itemType\":\"conditionType\"," + + "\"metadata\":{\"id\":\"booleanCondition\",\"description\":\"" + OLD_CONDITION_MARKER + "\"}}"); + putDoc(client, INDEX_SYSTEMITEMS, USER_RULE_ID, + "{\"itemId\":\"" + USER_RULE_ID + "\",\"itemType\":\"rule\",\"scope\":\"systemscope\"," + + "\"metadata\":{\"id\":\"" + USER_RULE_ID + "\",\"name\":\"UNOMI-998 user rule\"}}"); + putDoc(client, INDEX_SYSTEMITEMS, USER_SEGMENT_ID, + "{\"itemId\":\"" + USER_SEGMENT_ID + "\",\"itemType\":\"segment\",\"scope\":\"systemscope\"," + + "\"metadata\":{\"id\":\"" + USER_SEGMENT_ID + "\",\"name\":\"UNOMI-998 user segment\"}}"); + + putDoc(client, INDEX_GEONAME, GEO_ID, + "{\"itemId\":\"" + GEO_ID + "\",\"itemType\":\"geonameEntry\",\"name\":\"Paris\"}"); + putDoc(client, INDEX_CLUSTERNODE, NODE_ID, + "{\"itemId\":\"" + NODE_ID + "\",\"itemType\":\"clusterNode\",\"scope\":\"system\"}"); + putDoc(client, INDEX_SFDC, SFDC_ID, + "{\"itemId\":\"" + SFDC_ID + "\",\"itemType\":\"sfdcConfiguration\",\"scope\":\"system\"}"); + putDoc(client, INDEX_PROFILE, PROFILE_ID, + "{\"itemId\":\"" + PROFILE_ID + "\",\"itemType\":\"profile\",\"properties\":{\"firstName\":\"Ada\"}}"); + + HttpUtils.executePostRequest(client, es + "/_refresh", null, null); + } + + private void assertMigrationBundleShipsEngineMappings() { + Bundle migrationBundle = FrameworkUtil.getBundle(MigrationUtils.class); + Assert.assertNotNull(migrationBundle); + for (String engine : Arrays.asList("elasticsearch", "opensearch")) { + for (String mappingFile : Arrays.asList("clusterNode.json", "event.json", "geonameEntry.json", + "personaSession.json", "profile.json", "profileAlias.json", "session.json", "systemItems.json", "tenant.json")) { + String path = "META-INF/cxs/migration-mappings/" + engine + "/" + mappingFile; + Assert.assertNotNull("boot migrate needs " + path + " in shell-commands", + migrationBundle.getEntry(path)); + } + } + } + + private void putDoc(CloseableHttpClient client, String index, String id, String source) throws Exception { + HttpUtils.executePutRequest(client, + getSearchEngineBaseUrl() + "/" + index + "/_doc/" + id + "?refresh=true", source, null); + } + + private JsonNode getByDocumentId(String index, String documentId) throws Exception { + try { + String response = HttpUtils.executeGetRequest(httpClient, + getSearchEngineBaseUrl() + "/" + index + "/_doc/" + documentId, null); + if (response == null || response.isBlank()) { + return null; + } + JsonNode node = getObjectMapper().readTree(response); + if (!node.path("found").asBoolean(false)) { + return null; + } + return node; + } catch (HttpRequestException e) { + // HttpUtils throws on HTTP 404 when the document is missing + if (e.getCode() != 404) { + throw e; + } + return null; + } + } + + private JsonNode findByItemId(String index, String itemId) throws Exception { + // After migrate reindex, string fields use the folding text mapping with a .keyword subfield. + // Term/prefix on the analyzed field miss camelCase ids such as booleanCondition. + String body = "{\"size\":20,\"query\":{\"bool\":{\"should\":[" + + "{\"term\":{\"itemId.keyword\":\"" + itemId + "\"}}," + + "{\"prefix\":{\"itemId.keyword\":\"" + itemId + "_\"}}," + + "{\"term\":{\"itemId\":\"" + itemId + "\"}}," + + "{\"prefix\":{\"itemId\":\"" + itemId + "_\"}}" + + "],\"minimum_should_match\":1}}}"; + JsonNode hits = getObjectMapper().readTree(HttpUtils.executePostRequest(httpClient, + getSearchEngineBaseUrl() + "/" + index + "/_search", body, null)) + .path("hits").path("hits"); + for (JsonNode hit : hits) { + String found = hit.path("_source").path("itemId").asText(); + String docId = hit.path("_id").asText(); + if (found.equals(itemId) || found.startsWith(itemId + "_") + || docId.equals(itemId) || docId.contains("_" + itemId + "_") + || docId.endsWith("_" + itemId)) { + return hit; + } + } + return null; + } +} diff --git a/itests/src/test/java/org/apache/unomi/itests/persistence/OpenSearchITBackend.java b/itests/src/test/java/org/apache/unomi/itests/persistence/OpenSearchITBackend.java index 04e182264..4c225602f 100644 --- a/itests/src/test/java/org/apache/unomi/itests/persistence/OpenSearchITBackend.java +++ b/itests/src/test/java/org/apache/unomi/itests/persistence/OpenSearchITBackend.java @@ -82,15 +82,25 @@ public Option[] featureOptions() { @Override public Option[] configurationOptions() { String port = searchPort(); + String address = "localhost:" + port; return new Option[]{ editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.cluster.name", "contextElasticSearchITests"), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.addresses", "localhost:" + port), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.addresses", address), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.username", OS_USER), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.password", OS_PASSWORD), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.sslEnable", "false"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.sslTrustAllCertificates", "true"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.rollover.maxDocs", "300"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.minimalClusterState", "YELLOW"), + // org.apache.unomi.migration.cfg still resolves connection settings from the + // elasticsearch.* placeholders. Mirror OpenSearch here so unomi:migrate (e.g. + // Migrate30ToCurrentVersionIT) does not fall back to localhost:9200. + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.addresses", address), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.username", OS_USER), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.password", OS_PASSWORD), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.sslEnable", "false"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.sslTrustAllCertificates", "true"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.rollover.maxDocs", "300"), }; } diff --git a/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy index dada0f0f4..2cb53fdac 100644 --- a/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy +++ b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy @@ -107,10 +107,10 @@ ${sep} Generated : ${isoDate} Tenant ID : ${tenantId} -PUBLIC KEY (X-Unomi-Public-Key header -- context.json / event collector): +PUBLIC KEY (X-Unomi-Api-Key header -- context.json / event collector): ${generatedPublicKey} -PRIVATE KEY (X-Unomi-Key header -- protected / admin endpoints): +PRIVATE KEY (HTTP Basic authentication as tenantId:privateKey -- protected / admin endpoints): ${generatedPrivateKey} IMPORTANT: These keys cannot be recovered after this file is deleted. diff --git a/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg b/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg index ef347295a..62423a6af 100644 --- a/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg +++ b/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg @@ -17,7 +17,9 @@ # Migration config used for silent migration -# Various configuration related to ElasticSearch communication to be able to connect and migrate the data +# Various configuration related to search-engine communication to be able to connect and migrate the data. +# These keys read the org.apache.unomi.elasticsearch.* system properties for historical reasons, also when +# the search engine is OpenSearch: set them to your OpenSearch connection settings before running unomi:migrate. esAddresses = ${org.apache.unomi.elasticsearch.addresses:-localhost:9200} esSSLEnabled = ${org.apache.unomi.elasticsearch.sslEnable:-false} esLogin = ${org.apache.unomi.elasticsearch.username:-} diff --git a/tools/shell-commands/src/main/resources/requestBody/4.0.0/initialize_tenant_and_audit_fields.painless b/tools/shell-commands/src/main/resources/requestBody/4.0.0/initialize_tenant_and_audit_fields.painless index 97dcd9b38..455735b03 100644 --- a/tools/shell-commands/src/main/resources/requestBody/4.0.0/initialize_tenant_and_audit_fields.painless +++ b/tools/shell-commands/src/main/resources/requestBody/4.0.0/initialize_tenant_and_audit_fields.painless @@ -20,12 +20,18 @@ // stay in the configured tenant even when their 3.0 scope is "systemscope". // A shipped definition is recognised by item type and id only, so a 3.0 item sharing both // (such as a built-in rule edited in place) also goes to system. +// The reindex reads from the "-cloned" backup and ctx._index is that source index, so drop the suffix. +def indexLower = ctx._index != null ? ctx._index.toLowerCase(Locale.ROOT) : ''; +if (indexLower.endsWith('-cloned')) { + indexLower = indexLower.substring(0, indexLower.length() - '-cloned'.length()); +} + boolean useSystemTenant = false; if (params.forceDefaultTenant != true) { def itemTypeLower = ctx._source.itemType != null ? ctx._source.itemType.toLowerCase(Locale.ROOT) : ''; // Shipped ids are grouped by item type, so other types (profiles, events...) skip the lookup def shippedIds = params.shippedDefinitionIds != null ? params.shippedDefinitionIds.get(itemTypeLower) : null; - if (itemTypeLower == 'geonameentry' || (ctx._index != null && ctx._index.toLowerCase(Locale.ROOT).endsWith('-geonameentry'))) { + if (itemTypeLower == 'geonameentry' || indexLower.endsWith('-geonameentry')) { useSystemTenant = true; } else if (shippedIds != null) { def rawId = ctx._source.itemId != null ? ctx._source.itemId : ctx._id; @@ -49,7 +55,7 @@ if (ctx._id != null) { // Skip if already has tenant prefix if (!ctx._id.startsWith(params.tenantId + '_') && !ctx._id.startsWith(params.systemTenantId + '_')) { def baseId = ctx._id; - if (ctx._index != null && ctx._index.toLowerCase(Locale.ROOT).endsWith('-systemitems')) { + if (indexLower.endsWith('-systemitems')) { // For system items the document ID is tenantId_itemId_itemtype (see getDocumentIDForItemType). // 3.0 document IDs already carry the lower-case _itemtype suffix, so only add it when missing. def itemType = ctx._source.itemType != null ? ctx._source.itemType.toLowerCase(Locale.ROOT) : null; @@ -69,7 +75,6 @@ if (ctx._id != null) { // systemitems must get audit here: UNOMI-997 puts shipped defs under the system // tenant before Unomi starts, so bundle deploy treats them as updates and would // otherwise leave createdBy unset on docs that never had it. -def indexLower = ctx._index != null ? ctx._index.toLowerCase(Locale.ROOT) : ''; if (!indexLower.endsWith('-geonameentry') && !indexLower.endsWith('-geonames')) { // Handle creation date based on item type if (ctx._source.creationDate == null) {