This is an automated email from the ASF dual-hosted git repository.
sergehuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/master by this push:
new 670319cce UNOMI-998: Cover a 3.0 to 4.0 migration in the integration
tests (#910)
670319cce is described below
commit 670319cce67abc20c3b95a78e9f7c3eaa4279ace
Author: Serge Huber <[email protected]>
AuthorDate: Thu Oct 8 12:29:59 2026 +0200
UNOMI-998: Cover a 3.0 to 4.0 migration in the integration tests (#910)
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../test/java/org/apache/unomi/itests/AllITs.java | 2 +
.../apache/unomi/itests/CorePersistenceITs.java | 2 +
.../migration/Migrate16xToCurrentVersionIT.java | 11 +
.../migration/Migrate30ToCurrentVersionIT.java | 375 +++++++++++++++++++++
.../itests/persistence/OpenSearchITBackend.java | 12 +-
.../migrate-4.0.0-10-tenantInitialization.groovy | 4 +-
.../main/resources/org.apache.unomi.migration.cfg | 4 +-
.../initialize_tenant_and_audit_fields.painless | 11 +-
8 files changed, 414 insertions(+), 7 deletions(-)
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 @@ package org.apache.unomi.itests;
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 @@ import org.junit.runners.Suite.SuiteClasses;
*/
@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 @@ package org.apache.unomi.itests;
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 @@ import org.junit.runners.Suite.SuiteClasses;
*/
@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.Logger;
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 class Migrate16xToCurrentVersionIT extends BaseIT {
// 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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_<id>_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<Path> 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 class OpenSearchITBackend implements
PersistenceITBackend {
@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) {