This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch UNOMI-996-migration-mappings
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/UNOMI-996-migration-mappings
by this push:
new 7812c092b UNOMI-996: ship migration mappings in the bundle and fail on
missing ones
7812c092b is described below
commit 7812c092bbcee30e050eb29f5b55047858e3203c
Author: Serge Huber <[email protected]>
AuthorDate: Tue Oct 6 18:07:39 2026 +0200
UNOMI-996: ship migration mappings in the bundle and fail on missing ones
The copied mappings never reached the shell-commands jar, and the new
fallback hid it by silently reusing the 3.0 mapping of known types.
- Package the Elasticsearch and OpenSearch mappings as resources under
META-INF/cxs/migration-mappings/{engine}, outside the folder the
persistence services scan, and pick the engine from the cluster.
- Known types must find their bundled mapping; only unknown types copy
the mapping stored on the index. Print the choice for each index.
- Match item types on the whole index name instead of a prefix and add
a profileAlias configuration.
- Tests: assert the mappings in the migration bundle itself, cover the
system-scope path, and reindex through MigrationUtils.moveToIndex.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../itests/migration/Migrate40IndexMappingIT.java | 114 +++++++++++--------
tools/shell-commands/pom.xml | 47 ++++----
.../shell/migration/utils/MigrationUtils.java | 126 ++++++++-------------
.../migrate-4.0.0-01-tenantDocumentIds.groovy | 23 +++-
.../migrate-4.0.0-10-tenantInitialization.groovy | 3 +-
.../shell/migration/utils/MigrationUtilsTest.java | 88 +++++++++-----
6 files changed, 216 insertions(+), 185 deletions(-)
diff --git
a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate40IndexMappingIT.java
b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate40IndexMappingIT.java
index c7970ce8e..b66b472f5 100644
---
a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate40IndexMappingIT.java
+++
b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate40IndexMappingIT.java
@@ -18,8 +18,6 @@ package org.apache.unomi.itests.migration;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.unomi.itests.BaseIT;
import org.apache.unomi.itests.persistence.SearchBackendIT;
@@ -30,9 +28,14 @@ import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
import org.junit.experimental.categories.Category;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
/**
* UNOMI-996: migration must find bundled mappings (including cluster nodes)
and
@@ -57,23 +60,66 @@ public class Migrate40IndexMappingIT extends BaseIT {
}
@Test
- public void clusterNodeMappingIsBundledWithTheMigrationCommand() throws
Exception {
- String mapping = MigrationUtils.extractMappingFromBundles(
-
FrameworkUtil.getBundle(MigrationUtils.class).getBundleContext(),
"clusterNode.json");
- Assert.assertTrue("clusterNode mapping should define cpuLoad",
mapping.contains("cpuLoad"));
+ public void mappingsAreBundledWithTheMigrationCommandForBothEngines() {
+ // Look in the migration command bundle only: the started persistence
bundle ships these files too
+ Bundle migrationBundle = FrameworkUtil.getBundle(MigrationUtils.class);
+ for (String engine : Arrays.asList(SEARCH_ENGINE_ELASTICSEARCH,
SEARCH_ENGINE_OPENSEARCH)) {
+ for (String mappingFile : Arrays.asList("clusterNode.json",
"profile.json", "event.json", "geonameEntry.json")) {
+ String path = "META-INF/cxs/migration-mappings/" + engine +
"/" + mappingFile;
+ Assert.assertNotNull(path + " should be shipped with the
migration command", migrationBundle.getEntry(path));
+ }
+ }
+ }
+
+ @Test
+ public void searchEngineIsDetected() throws Exception {
+ assumeHttpAdminApi();
+ try (CloseableHttpClient httpClient = createSearchEngineHttpClient()) {
+ Assert.assertEquals(getPersistenceBackend().providerId(),
+ MigrationUtils.getSearchEngine(httpClient,
getSearchEngineBaseUrl()));
+ }
}
@Test
public void unknownIndexKeepsDocumentsOnTheConfiguredTenant() throws
Exception {
+ JsonNode hits = migrateUnknownIndex(true);
+ Assert.assertEquals(2, hits.size());
+ for (JsonNode hit : hits) {
+ String id = hit.path("_id").asText();
+ Assert.assertTrue("document id should use the configured tenant: "
+ id, id.startsWith(TEST_TENANT_ID + "_"));
+ Assert.assertEquals(TEST_TENANT_ID,
hit.path("_source").path("tenantId").asText());
+ }
+ }
+
+ @Test
+ public void knownIndexKeepsSystemScopeDocumentsOnTheSystemTenant() throws
Exception {
+ JsonNode hits = migrateUnknownIndex(false);
+ Assert.assertEquals(2, hits.size());
+ for (JsonNode hit : hits) {
+ boolean systemScope =
"system".equals(hit.path("_source").path("scope").asText());
+ String expectedTenant = systemScope ? "system" : TEST_TENANT_ID;
+ Assert.assertEquals(expectedTenant + "_" +
hit.path("_source").path("itemId").asText(), hit.path("_id").asText());
+ Assert.assertEquals(expectedTenant,
hit.path("_source").path("tenantId").asText());
+ }
+ }
+
+ private void assumeHttpAdminApi() {
Assume.assumeTrue(
"HTTP admin API required to create migration fixture indices
(provider="
+ getPersistenceBackend().providerId() + ")",
persistenceCapabilities().httpAdminApi());
+ }
+
+ /**
+ * Reindexes a fixture index holding one document without scope and one
with scope "system"
+ * through the 4.0 tenant script, the way the migration does, and returns
the resulting hits.
+ */
+ private JsonNode migrateUnknownIndex(boolean forceDefaultTenant) throws
Exception {
+ assumeHttpAdminApi();
try (CloseableHttpClient httpClient = createSearchEngineHttpClient()) {
String es = getSearchEngineBaseUrl();
- org.osgi.framework.BundleContext migrationBundles =
-
FrameworkUtil.getBundle(MigrationUtils.class).getBundleContext();
+ BundleContext migrationBundles =
FrameworkUtil.getBundle(MigrationUtils.class).getBundleContext();
String sourceSettings = "{"
+
"\"settings\":{\"number_of_shards\":1,\"number_of_replicas\":0},"
@@ -91,55 +137,33 @@ public class Migrate40IndexMappingIT extends BaseIT {
+ "/_doc/unknown2?refresh=true",
"{\"itemId\":\"unknown2\",\"itemType\":\"sfdcConfiguration\",\"scope\":\"system\"}",
null);
- String mapping = MigrationUtils.resolveIndexMapping(
- migrationBundles, httpClient, es, UNKNOWN_INDEX,
"does-not-exist.json");
+ String mapping =
MigrationUtils.extractMappingFromIndex(httpClient, es, UNKNOWN_INDEX);
Assert.assertTrue("copied mapping should keep itemType",
mapping.contains("itemType"));
- Assert.assertEquals("unomi996unknown",
- MigrationUtils.resolveItemType(UNKNOWN_INDEX, "context",
- Arrays.asList("profile", "event", "clusterNode",
"generic")));
+ String itemType = MigrationUtils.resolveItemType(UNKNOWN_INDEX,
"context",
+ Arrays.asList("profile", "event", "clusterNode",
"generic"));
+ Assert.assertEquals("unomi996unknown", itemType);
String destSettings =
"{\"settings\":{\"number_of_shards\":1,\"number_of_replicas\":0},\"mappings\":"
+ mapping + "}";
MigrationUtils.createIndex(httpClient, es, DEST_INDEX,
destSettings);
+ Map<String, Object> params = new HashMap<>();
+ params.put("tenantId", TEST_TENANT_ID);
+ params.put("systemTenantId", "system");
+ params.put("forceDefaultTenant", forceDefaultTenant);
+ params.put("itemType", itemType);
+ params.put("date", "2026-01-01T00:00:00Z");
+ params.put("systemItems", Collections.emptySet());
+
String painless = MigrationUtils.getFileWithoutComments(
migrationBundles,
"requestBody/4.0.0/initialize_tenant_and_audit_fields.painless");
- ObjectMapper mapper = new ObjectMapper();
- ObjectNode paramsObj = mapper.createObjectNode();
- paramsObj.put("tenantId", TEST_TENANT_ID);
- paramsObj.put("systemTenantId", "system");
- paramsObj.put("forceDefaultTenant", true);
- paramsObj.put("itemType", "generic");
- paramsObj.put("date", "2026-01-01T00:00:00Z");
- paramsObj.set("systemItems", mapper.createArrayNode());
-
- ObjectNode scriptObj = mapper.createObjectNode();
- scriptObj.put("source", painless);
- scriptObj.put("lang", "painless");
- scriptObj.set("params", paramsObj);
-
- ObjectNode reindex = mapper.createObjectNode();
- reindex.set("source", mapper.createObjectNode().put("index",
UNKNOWN_INDEX));
- reindex.set("dest", mapper.createObjectNode().put("index",
DEST_INDEX));
- reindex.set("script", scriptObj);
- HttpUtils.executePostRequest(httpClient, es +
"/_reindex?refresh=true&wait_for_completion=true",
- mapper.writeValueAsString(reindex), null);
+ MigrationUtils.moveToIndex(httpClient, migrationBundles, es,
UNKNOWN_INDEX, DEST_INDEX, painless, params);
+ HttpUtils.executePostRequest(httpClient, es + "/" + DEST_INDEX +
"/_refresh", null, null);
String search = HttpUtils.executeGetRequest(httpClient,
es + "/" + DEST_INDEX + "/_search?size=10", null);
- ArrayNode hits = (ArrayNode)
mapper.readTree(search).path("hits").path("hits");
- Assert.assertEquals(2, hits.size());
- for (JsonNode hit : hits) {
- String id = hit.path("_id").asText();
- JsonNode source = hit.path("_source");
- Assert.assertTrue("document id should use the configured
tenant: " + id,
- id.startsWith(TEST_TENANT_ID + "_"));
- Assert.assertFalse("unknown index documents must not use the
system tenant: " + id,
- id.startsWith("system_"));
- Assert.assertEquals(TEST_TENANT_ID,
source.path("tenantId").asText());
- Assert.assertNotEquals("system",
source.path("tenantId").asText());
- }
+ return new
ObjectMapper().readTree(search).path("hits").path("hits");
}
}
}
diff --git a/tools/shell-commands/pom.xml b/tools/shell-commands/pom.xml
index 4f0aef63b..17ebbcced 100644
--- a/tools/shell-commands/pom.xml
+++ b/tools/shell-commands/pom.xml
@@ -157,31 +157,30 @@
</dependencies>
<build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ </resource>
+ <!-- Mappings needed by the migration before Unomi is started.
Kept out of META-INF/cxs/mappings
+ so the persistence services do not load them as predefined
mappings. -->
+ <resource>
+
<directory>../../persistence-elasticsearch/core/src/main/resources/META-INF/cxs/mappings</directory>
+
<targetPath>META-INF/cxs/migration-mappings/elasticsearch</targetPath>
+ </resource>
+ <resource>
+
<directory>../../extensions/geonames/services/src/main/resources/META-INF/cxs/mappings</directory>
+
<targetPath>META-INF/cxs/migration-mappings/elasticsearch</targetPath>
+ </resource>
+ <resource>
+
<directory>../../persistence-opensearch/core/src/main/resources/META-INF/cxs/mappings</directory>
+
<targetPath>META-INF/cxs/migration-mappings/opensearch</targetPath>
+ </resource>
+ <resource>
+
<directory>../../extensions/geonames/services/src/main/resources/META-INF/cxs/mappings</directory>
+
<targetPath>META-INF/cxs/migration-mappings/opensearch</targetPath>
+ </resource>
+ </resources>
<plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-resources-plugin</artifactId>
- <executions>
- <execution>
- <id>copy-persistence-mappings</id>
- <phase>generate-resources</phase>
- <goals>
- <goal>copy-resources</goal>
- </goals>
- <configuration>
-
<outputDirectory>${project.build.outputDirectory}/META-INF/cxs/mappings</outputDirectory>
- <resources>
- <resource>
-
<directory>${project.basedir}/../../persistence-elasticsearch/core/src/main/resources/META-INF/cxs/mappings</directory>
- </resource>
- <resource>
-
<directory>${project.basedir}/../../extensions/geonames/services/src/main/resources/META-INF/cxs/mappings</directory>
- </resource>
- </resources>
- </configuration>
- </execution>
- </executions>
- </plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
diff --git
a/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/utils/MigrationUtils.java
b/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/utils/MigrationUtils.java
index f2d33be07..4e8d71657 100644
---
a/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/utils/MigrationUtils.java
+++
b/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/utils/MigrationUtils.java
@@ -303,113 +303,66 @@ public class MigrationUtils {
}
public static String extractMappingFromBundles(BundleContext
bundleContext, String fileName) throws IOException {
- String mapping = findMappingInBundles(bundleContext, fileName);
- if (mapping == null) {
- throw new RuntimeException("no mapping found in bundles for: " +
fileName);
- }
- return mapping;
+ return extractMappingFromBundles(bundleContext, fileName,
"elasticsearch");
}
/**
- * Looks up {@code META-INF/cxs/mappings/{fileName}} in every OSGi bundle.
- * The migration command bundle ships a copy of the persistence mappings
so this
- * works before Unomi persistence features are started.
+ * Looks up a mapping file in every OSGi bundle: first in {@code
META-INF/cxs/mappings}
+ * (the persistence bundles, when installed), then in the per-engine copy
shipped with the
+ * migration command bundle, which is available before Unomi is started.
The copy lives in a
+ * separate folder so the persistence services do not load it as a
predefined mapping.
*
- * @return the mapping JSON, or {@code null} if no bundle contains the file
+ * @param searchEngine {@code elasticsearch} or {@code opensearch}, see
{@link #getSearchEngine}
*/
- public static String findMappingInBundles(BundleContext bundleContext,
String fileName) throws IOException {
- if (bundleContext == null || fileName == null || fileName.isBlank()) {
- return null;
- }
- for (Bundle bundle : bundleContext.getBundles()) {
- Enumeration<URL> predefinedMappings =
bundle.findEntries("META-INF/cxs/mappings", fileName, true);
- if (predefinedMappings == null) {
- continue;
- }
- if (predefinedMappings.hasMoreElements()) {
- URL predefinedMappingURL = predefinedMappings.nextElement();
- return IOUtils.toString(predefinedMappingURL,
StandardCharsets.UTF_8);
+ public static String extractMappingFromBundles(BundleContext
bundleContext, String fileName, String searchEngine) throws IOException {
+ for (String path : List.of("META-INF/cxs/mappings",
"META-INF/cxs/migration-mappings/" + searchEngine)) {
+ for (Bundle bundle : bundleContext.getBundles()) {
+ Enumeration<URL> predefinedMappings = bundle.findEntries(path,
fileName, true);
+ if (predefinedMappings != null &&
predefinedMappings.hasMoreElements()) {
+ return IOUtils.toString(predefinedMappings.nextElement(),
StandardCharsets.UTF_8);
+ }
}
}
- return null;
+ throw new RuntimeException("no mapping found in bundles for: " +
fileName);
}
/**
- * Resolves the mapping used to recreate an index during migration.
- * Prefers a bundled file when present (the 4.0 definition). Otherwise
copies
- * the mapping currently stored on the index, so unknown types do not stop
the run.
+ * Copies the mapping currently stored on an index (GET {@code
/{index}/_mapping}).
+ * Used for indices that have no bundled 4.0 mapping.
*/
- public static String resolveIndexMapping(BundleContext bundleContext,
CloseableHttpClient httpClient,
- String esAddress, String
indexName, String mappingFileName) throws IOException {
- if (mappingFileName != null && !mappingFileName.isBlank()) {
- String bundled = findMappingInBundles(bundleContext,
mappingFileName);
- if (bundled != null) {
- return bundled;
- }
- }
- return extractMappingFromIndex(httpClient, esAddress, indexName);
- }
-
public static String extractMappingFromIndex(CloseableHttpClient
httpClient, String esAddress, String indexName) throws IOException {
- if (indexName == null || indexName.isBlank()) {
- throw new IllegalArgumentException("An index name is required to
copy its mapping");
- }
- String response = HttpUtils.executeGetRequest(httpClient, esAddress +
"/" + indexName + "/_mapping", null);
- return mappingsFromIndexResponse(response);
+ return
mappingsFromIndexResponse(HttpUtils.executeGetRequest(httpClient, esAddress +
"/" + indexName + "/_mapping", null));
}
- /**
- * Reads the mappings object from a GET {@code /{index}/_mapping} body.
- */
- public static String mappingsFromIndexResponse(String responseBody) {
+ static String mappingsFromIndexResponse(String responseBody) {
if (responseBody == null || responseBody.isBlank()) {
throw new IllegalArgumentException("Index mapping response is
empty");
}
JSONObject root = new JSONObject(responseBody);
- if (root.keySet().isEmpty()) {
- throw new IllegalArgumentException("Index mapping response has no
indices");
- }
- String firstIndex = root.keys().next();
- JSONObject index = root.getJSONObject(firstIndex);
- if (!index.has("mappings")) {
- throw new IllegalArgumentException("Index mapping response has no
mappings for " + firstIndex);
+ if (root.length() != 1) {
+ throw new IllegalArgumentException("Expected the mapping of
exactly one index, got: " + root.keySet());
}
- return index.getJSONObject("mappings").toString();
+ return
root.getJSONObject(root.keys().next()).getJSONObject("mappings").toString();
}
/**
* Maps an index name such as {@code context-event-000001} or {@code
context-clusternode}
- * to an item type. Known types are matched longest-first, ignoring case.
- * Unknown names keep the suffix after the prefix (without a rollover
number).
+ * to an item type. The name after the prefix (without a rollover number)
must equal a known
+ * type, ignoring case; otherwise that name is returned as is. Names that
do not start with
+ * the prefix resolve to {@code generic}.
*/
public static String resolveItemType(String indexName, String indexPrefix,
Collection<String> knownTypes) {
- if (indexName == null || indexName.isBlank()) {
- throw new IllegalArgumentException("An index name is required");
+ String prefix = indexPrefix + "-";
+ if (!indexName.regionMatches(true, 0, prefix, 0, prefix.length())) {
+ return "generic";
}
- if (indexPrefix == null || indexPrefix.isBlank()) {
- throw new IllegalArgumentException("An index prefix is required");
- }
- String lowerIndex = indexName.toLowerCase(Locale.ROOT);
- String prefix = indexPrefix.toLowerCase(Locale.ROOT) + "-";
- if (knownTypes != null) {
- List<String> types = new ArrayList<>();
- for (String type : knownTypes) {
- if (type != null && !type.isBlank() &&
!"generic".equals(type)) {
- types.add(type);
- }
- }
- types.sort((a, b) -> Integer.compare(b.length(), a.length()));
- for (String type : types) {
- if (lowerIndex.startsWith(prefix +
type.toLowerCase(Locale.ROOT))) {
- return type;
- }
+ String name =
indexName.substring(prefix.length()).replaceFirst("-\\d{6}$", "");
+ for (String type : knownTypes) {
+ if (name.equalsIgnoreCase(type)) {
+ return type;
}
}
- if (lowerIndex.startsWith(prefix)) {
- String rest = indexName.substring(indexPrefix.length() + 1);
- return rest.replaceFirst("-\\d{6}$", "");
- }
- return "generic";
+ return name;
}
public static String buildIndexCreationRequest(String baseIndexSettings,
String mapping, MigrationContext context, boolean isMonthlyIndex) throws
IOException {
@@ -1015,6 +968,21 @@ public class MigrationUtils {
return version.split("\\.")[0]; // Return major version number
}
+ /**
+ * Detects the engine behind the given address from its root endpoint.
+ *
+ * @return {@code opensearch} or {@code elasticsearch}
+ */
+ public static String getSearchEngine(CloseableHttpClient httpClient,
String esAddress) throws IOException {
+ return
searchEngineFromRootResponse(HttpUtils.executeGetRequest(httpClient, esAddress,
null));
+ }
+
+ static String searchEngineFromRootResponse(String responseBody) {
+ // Only OpenSearch reports a distribution
+ String distribution = new
JSONObject(responseBody).getJSONObject("version").optString("distribution");
+ return "opensearch".equals(distribution) ? "opensearch" :
"elasticsearch";
+ }
+
public interface ScrollCallback {
void execute(String hits);
}
diff --git
a/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-01-tenantDocumentIds.groovy
b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-01-tenantDocumentIds.groovy
index b8557b50e..f5f265e3d 100644
---
a/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-01-tenantDocumentIds.groovy
+++
b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-01-tenantDocumentIds.groovy
@@ -67,15 +67,22 @@ def indexConfigs = [
mapping: "personaSession.json",
useRollover: false
],
+ "profileAlias": [
+ baseSettings: "requestBody/2.0.0/base_index_mapping.json",
+ mapping: "profileAlias.json",
+ useRollover: false
+ ],
"clusterNode": [
baseSettings: "requestBody/2.0.0/base_index_mapping.json",
mapping: "clusterNode.json",
- useRollover: false
+ useRollover: false,
+ forceDefaultTenant: true
],
"generic": [
baseSettings: "requestBody/2.0.0/base_index_mapping.json",
mapping: null, // Copy the mapping currently stored on the
index
- useRollover: false
+ useRollover: false,
+ forceDefaultTenant: true
]
]
@@ -113,6 +120,9 @@ context.performMigrationStep("4.0.0-get-all-indices", () ->
{
// Get the Painless script
String updateScript = MigrationUtils.getFileWithoutComments(bundleContext,
"requestBody/4.0.0/initialize_tenant_and_audit_fields.painless")
+ String searchEngine =
MigrationUtils.getSearchEngine(context.getHttpClient(), esAddress)
+ context.printMessage("Search engine: " + searchEngine)
+
// Process each index (reindex them)
allIndices.each { indexName ->
context.printMessage("Processing index: " + indexName)
@@ -125,13 +135,16 @@ context.performMigrationStep("4.0.0-get-all-indices", ()
-> {
Map<String, Object> params = new HashMap<>(baseParams)
params.put("itemType", itemType)
// Cluster nodes and unknown indices keep site data in the configured
tenant, never "system"
- boolean forceDefaultTenant = (itemType == "clusterNode" ||
indexConfig.mapping == null)
+ boolean forceDefaultTenant = indexConfig.forceDefaultTenant == true
params.put("forceDefaultTenant", forceDefaultTenant)
// Get base settings and mapping
String baseSettings = MigrationUtils.resourceAsString(bundleContext,
indexConfig.baseSettings)
- String mappingFile = indexConfig.mapping ?: "${itemType}.json"
- String mapping = MigrationUtils.resolveIndexMapping(bundleContext,
context.getHttpClient(), esAddress, indexName, mappingFile)
+ // A known type must find its bundled 4.0 mapping; only unknown types
copy the mapping stored on the index
+ String mapping = indexConfig.mapping ?
+ MigrationUtils.extractMappingFromBundles(bundleContext,
indexConfig.mapping, searchEngine) :
+
MigrationUtils.extractMappingFromIndex(context.getHttpClient(), esAddress,
indexName)
+ context.printMessage("Item type: ${itemType}, mapping:
${indexConfig.mapping ?: 'copied from the index'}, default tenant only:
${forceDefaultTenant}")
// Build index settings
String newIndexSettings
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 995f09d69..dada0f0f4 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
@@ -71,7 +71,8 @@ if (Files.exists(secretsDir)) {
// Create the default tenant index and items
context.performMigrationStep("4.0.0-create-tenant-index", () -> {
String baseSettings = MigrationUtils.resourceAsString(bundleContext,
"requestBody/2.0.0/base_index_mapping.json")
- String mapping = MigrationUtils.extractMappingFromBundles(bundleContext,
"tenant.json")
+ String mapping = MigrationUtils.extractMappingFromBundles(bundleContext,
"tenant.json",
+ MigrationUtils.getSearchEngine(context.getHttpClient(), esAddress))
String newIndexSettings =
MigrationUtils.buildIndexCreationRequest(baseSettings, mapping, context, false)
if (!MigrationUtils.indexExists(context.getHttpClient(), esAddress,
"${indexPrefix}-tenant")) {
diff --git
a/tools/shell-commands/src/test/java/org/apache/unomi/shell/migration/utils/MigrationUtilsTest.java
b/tools/shell-commands/src/test/java/org/apache/unomi/shell/migration/utils/MigrationUtilsTest.java
index 2be643754..92b48578c 100644
---
a/tools/shell-commands/src/test/java/org/apache/unomi/shell/migration/utils/MigrationUtilsTest.java
+++
b/tools/shell-commands/src/test/java/org/apache/unomi/shell/migration/utils/MigrationUtilsTest.java
@@ -215,24 +215,57 @@ public class MigrationUtilsTest {
assertEquals("Resource not found: missing.painless", ex.getMessage());
}
- @Test
- public void
extractMappingFromBundlesFindsKnownTypeWithoutPersistenceBundle() throws
Exception {
+ private static final String MAPPING =
"{\"properties\":{\"cpuLoad\":{\"type\":\"double\"}}}";
+
+ private String extractClusterNodeMappingFrom(String path) throws Exception
{
+ return extractClusterNodeMappingFrom(path, "elasticsearch");
+ }
+
+ private String extractClusterNodeMappingFrom(String path, String
searchEngine) throws Exception {
Path mappingFile = Files.createTempFile("clusterNode", ".json");
- Files.writeString(mappingFile,
"{\"properties\":{\"cpuLoad\":{\"type\":\"double\"}}}");
- URL mappingUrl = mappingFile.toUri().toURL();
+ try {
+ Files.writeString(mappingFile, MAPPING);
+ when(bundleContext.getBundles()).thenReturn(new Bundle[]{bundle});
+ when(bundle.findEntries(path, "clusterNode.json", true))
+
.thenReturn(Collections.enumeration(Collections.singletonList(mappingFile.toUri().toURL())));
+ return MigrationUtils.extractMappingFromBundles(bundleContext,
"clusterNode.json", searchEngine);
+ } finally {
+ Files.deleteIfExists(mappingFile);
+ }
+ }
+
+ @Test
+ public void extractMappingFromBundlesFindsPersistenceMapping() throws
Exception {
+ assertEquals(MAPPING,
extractClusterNodeMappingFrom("META-INF/cxs/mappings"));
+ }
+
+ @Test
+ public void extractMappingFromBundlesFallsBackToMigrationCopy() throws
Exception {
+ assertEquals(MAPPING,
extractClusterNodeMappingFrom("META-INF/cxs/migration-mappings/elasticsearch"));
+ assertEquals(MAPPING,
extractClusterNodeMappingFrom("META-INF/cxs/migration-mappings/opensearch",
"opensearch"));
+ }
+
+ @Test
+ public void extractMappingFromBundlesIgnoresTheOtherEngineCopy() {
when(bundleContext.getBundles()).thenReturn(new Bundle[]{bundle});
- when(bundle.findEntries("META-INF/cxs/mappings", "clusterNode.json",
true))
-
.thenReturn(Collections.enumeration(Collections.singletonList(mappingUrl)));
+
when(bundle.findEntries("META-INF/cxs/migration-mappings/elasticsearch",
"event.json", true))
+
.thenReturn(Collections.enumeration(Collections.singletonList(getClass().getResource("/"))));
+
+ assertThrows(RuntimeException.class,
+ () -> MigrationUtils.extractMappingFromBundles(bundleContext,
"event.json", "opensearch"));
+ }
- String mapping =
MigrationUtils.extractMappingFromBundles(bundleContext, "clusterNode.json");
- assertEquals("{\"properties\":{\"cpuLoad\":{\"type\":\"double\"}}}",
mapping);
- Files.deleteIfExists(mappingFile);
+ @Test
+ public void searchEngineIsDetectedFromTheRootResponse() {
+ assertEquals("opensearch", MigrationUtils.searchEngineFromRootResponse(
+
"{\"version\":{\"distribution\":\"opensearch\",\"number\":\"3.0.0\"}}"));
+ assertEquals("elasticsearch",
MigrationUtils.searchEngineFromRootResponse(
+ "{\"version\":{\"number\":\"9.1.0\"}}"));
}
@Test
public void extractMappingFromBundlesThrowsWhenMissing() {
when(bundleContext.getBundles()).thenReturn(new Bundle[]{bundle});
- when(bundle.findEntries("META-INF/cxs/mappings", "generic.json",
true)).thenReturn(null);
RuntimeException ex = assertThrows(RuntimeException.class,
() -> MigrationUtils.extractMappingFromBundles(bundleContext,
"generic.json"));
@@ -240,40 +273,33 @@ public class MigrationUtilsTest {
}
@Test
- public void mappingsFromIndexResponseReadsFirstIndex() {
+ public void mappingsFromIndexResponseReadsTheIndexMappings() {
String body =
"{\"context-sfdcconfiguration\":{\"mappings\":{\"properties\":{\"itemId\":{\"type\":\"keyword\"}}}}}";
- String mappings = MigrationUtils.mappingsFromIndexResponse(body);
- assertEquals("{\"properties\":{\"itemId\":{\"type\":\"keyword\"}}}",
mappings);
+ assertEquals("{\"properties\":{\"itemId\":{\"type\":\"keyword\"}}}",
MigrationUtils.mappingsFromIndexResponse(body));
}
@Test
- public void mappingsFromIndexResponseRejectsEmptyBody() {
+ public void mappingsFromIndexResponseRejectsAnythingButOneIndex() {
assertThrows(IllegalArgumentException.class, () ->
MigrationUtils.mappingsFromIndexResponse(""));
assertThrows(IllegalArgumentException.class, () ->
MigrationUtils.mappingsFromIndexResponse("{}"));
+ assertThrows(IllegalArgumentException.class,
+ () ->
MigrationUtils.mappingsFromIndexResponse("{\"a\":{\"mappings\":{}},\"b\":{\"mappings\":{}}}"));
}
@Test
- public void resolveItemTypeMatchesClusterNodeIgnoringCase() {
- Collection<String> types = Arrays.asList("profile", "event",
"clusterNode", "generic");
+ public void
resolveItemTypeMatchesKnownTypesIgnoringCaseAndRolloverNumber() {
+ Collection<String> types = Arrays.asList("profile", "profileAlias",
"event", "clusterNode");
assertEquals("clusterNode",
MigrationUtils.resolveItemType("context-clusternode", "context", types));
- assertEquals("clusterNode",
MigrationUtils.resolveItemType("context-clusterNode", "context", types));
- }
-
- @Test
- public void resolveItemTypeMatchesRolloverEvent() {
- Collection<String> types = Arrays.asList("event", "session",
"generic");
+ assertEquals("profileAlias",
MigrationUtils.resolveItemType("context-profilealias", "context", types));
assertEquals("event",
MigrationUtils.resolveItemType("context-event-000001", "context", types));
}
@Test
- public void resolveItemTypeKeepsUnknownIndexSuffix() {
- Collection<String> types = Arrays.asList("profile", "event",
"generic");
+ public void resolveItemTypeDoesNotMatchOnKnownTypePrefix() {
+ Collection<String> types = Arrays.asList("profile", "event");
+ assertEquals("profilealias",
MigrationUtils.resolveItemType("context-profilealias", "context", types));
+ assertEquals("eventfoo",
MigrationUtils.resolveItemType("context-eventfoo-000001", "context", types));
assertEquals("sfdcconfiguration",
MigrationUtils.resolveItemType("context-sfdcconfiguration", "context", types));
+ assertEquals("generic",
MigrationUtils.resolveItemType("other-profile", "context", types));
}
-
- @Test
- public void resolveItemTypePrefersLongerKnownType() {
- Collection<String> types = Arrays.asList("profile", "profileAlias");
- assertEquals("profileAlias",
MigrationUtils.resolveItemType("context-profilealias", "context", types));
- }
-}
\ No newline at end of file
+}