Skip to content

Commit 2a53248

Browse files
committed
Merge remote-tracking branch 'mainBranch/main' into checkBeforeDelete
2 parents c83bfac + 14b9712 commit 2a53248

File tree

72 files changed

+3143
-493
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+3143
-493
lines changed

.github/workflows/CI-workflow.yml

-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ jobs:
3838
# using the same image which is used by opensearch-build team to build the OpenSearch Distribution
3939
# this image tag is subject to change as more dependencies and updates will arrive over time
4040
image: ${{ needs.Get-CI-Image-Tag.outputs.ci-image-version-linux }}
41-
# need to switch to root so that github actions can install runner binary on container without permission issues.
4241
options: ${{ needs.Get-CI-Image-Tag.outputs.ci-image-start-options }}
4342

4443
steps:

build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ subprojects {
9393

9494
configurations.all {
9595
// Force spotless depending on newer version of guava due to CVE-2023-2976. Remove after spotless upgrades.
96-
resolutionStrategy.force "com.google.guava:guava:32.1.2-jre"
96+
resolutionStrategy.force "com.google.guava:guava:32.1.3-jre"
9797
resolutionStrategy.force 'org.apache.commons:commons-compress:1.26.0'
9898
}
9999
}

common/build.gradle

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ dependencies {
2727
compileOnly group: 'com.google.code.gson', name: 'gson', version: '2.10.1'
2828
compileOnly group: 'org.json', name: 'json', version: '20231013'
2929
testImplementation group: 'org.json', name: 'json', version: '20231013'
30-
implementation('com.google.guava:guava:32.1.2-jre') {
30+
implementation('com.google.guava:guava:32.1.3-jre') {
3131
exclude group: 'com.google.guava', module: 'failureaccess'
3232
exclude group: 'com.google.code.findbugs', module: 'jsr305'
3333
exclude group: 'org.checkerframework', module: 'checker-qual'
@@ -36,6 +36,9 @@ dependencies {
3636
exclude group: 'com.google.guava', module: 'listenablefuture'
3737
}
3838
compileOnly 'com.jayway.jsonpath:json-path:2.9.0'
39+
compileOnly("com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}")
40+
compileOnly("com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}")
41+
compileOnly group: 'com.networknt' , name: 'json-schema-validator', version: '1.4.0'
3942
}
4043

4144
lombok {

common/src/main/java/org/opensearch/ml/common/CommonValue.java

+9-9
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,15 @@ public class CommonValue {
4545
public static final String TOOL_PARAMETERS_PREFIX = "tools.parameters.";
4646

4747
// Index mapping paths
48-
public static final String ML_MODEL_GROUP_INDEX_MAPPING_PATH = "index-mappings/ml-model-group.json";
49-
public static final String ML_MODEL_INDEX_MAPPING_PATH = "index-mappings/ml-model.json";
50-
public static final String ML_TASK_INDEX_MAPPING_PATH = "index-mappings/ml-task.json";
51-
public static final String ML_CONNECTOR_INDEX_MAPPING_PATH = "index-mappings/ml-connector.json";
52-
public static final String ML_CONFIG_INDEX_MAPPING_PATH = "index-mappings/ml-config.json";
53-
public static final String ML_CONTROLLER_INDEX_MAPPING_PATH = "index-mappings/ml-controller.json";
54-
public static final String ML_AGENT_INDEX_MAPPING_PATH = "index-mappings/ml-agent.json";
55-
public static final String ML_MEMORY_META_INDEX_MAPPING_PATH = "index-mappings/ml-memory-meta.json";
56-
public static final String ML_MEMORY_MESSAGE_INDEX_MAPPING_PATH = "index-mappings/ml-memory-message.json";
48+
public static final String ML_MODEL_GROUP_INDEX_MAPPING_PATH = "index-mappings/ml_model_group.json";
49+
public static final String ML_MODEL_INDEX_MAPPING_PATH = "index-mappings/ml_model.json";
50+
public static final String ML_TASK_INDEX_MAPPING_PATH = "index-mappings/ml_task.json";
51+
public static final String ML_CONNECTOR_INDEX_MAPPING_PATH = "index-mappings/ml_connector.json";
52+
public static final String ML_CONFIG_INDEX_MAPPING_PATH = "index-mappings/ml_config.json";
53+
public static final String ML_CONTROLLER_INDEX_MAPPING_PATH = "index-mappings/ml_controller.json";
54+
public static final String ML_AGENT_INDEX_MAPPING_PATH = "index-mappings/ml_agent.json";
55+
public static final String ML_MEMORY_META_INDEX_MAPPING_PATH = "index-mappings/ml_memory_meta.json";
56+
public static final String ML_MEMORY_MESSAGE_INDEX_MAPPING_PATH = "index-mappings/ml_memory_message.json";
5757

5858
// Calculate Versions independently of OpenSearch core version
5959
public static final Version VERSION_2_11_0 = Version.fromString("2.11.0");

common/src/main/java/org/opensearch/ml/common/connector/ConnectorAction.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ public ConnectorAction(
5555
String postProcessFunction
5656
) {
5757
if (actionType == null) {
58-
throw new IllegalArgumentException("action type can't null");
58+
throw new IllegalArgumentException("action type can't be null");
5959
}
6060
if (url == null) {
61-
throw new IllegalArgumentException("url can't null");
61+
throw new IllegalArgumentException("url can't be null");
6262
}
6363
if (method == null) {
64-
throw new IllegalArgumentException("method can't null");
64+
throw new IllegalArgumentException("method can't be null");
6565
}
6666
this.actionType = actionType;
6767
this.method = method;

common/src/main/java/org/opensearch/ml/common/conversation/Interaction.java

+12-4
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,18 @@ public XContentBuilder toXContent(XContentBuilder builder, ToXContentObject.Para
184184
builder.field(ActionConstants.CONVERSATION_ID_FIELD, conversationId);
185185
builder.field(ActionConstants.RESPONSE_INTERACTION_ID_FIELD, id);
186186
builder.field(ConversationalIndexConstants.INTERACTIONS_CREATE_TIME_FIELD, createTime);
187-
builder.field(ConversationalIndexConstants.INTERACTIONS_INPUT_FIELD, input);
188-
builder.field(ConversationalIndexConstants.INTERACTIONS_PROMPT_TEMPLATE_FIELD, promptTemplate);
189-
builder.field(ConversationalIndexConstants.INTERACTIONS_RESPONSE_FIELD, response);
190-
builder.field(ConversationalIndexConstants.INTERACTIONS_ORIGIN_FIELD, origin);
187+
if (input != null && !input.trim().isEmpty()) {
188+
builder.field(ConversationalIndexConstants.INTERACTIONS_INPUT_FIELD, input);
189+
}
190+
if (promptTemplate != null && !promptTemplate.trim().isEmpty()) {
191+
builder.field(ConversationalIndexConstants.INTERACTIONS_PROMPT_TEMPLATE_FIELD, promptTemplate);
192+
}
193+
if (response != null && !response.trim().isEmpty()) {
194+
builder.field(ConversationalIndexConstants.INTERACTIONS_RESPONSE_FIELD, response);
195+
}
196+
if (origin != null && !origin.trim().isEmpty()) {
197+
builder.field(ConversationalIndexConstants.INTERACTIONS_ORIGIN_FIELD, origin);
198+
}
191199
if (additionalInfo != null) {
192200
builder.field(ConversationalIndexConstants.INTERACTIONS_ADDITIONAL_INFO_FIELD, additionalInfo);
193201
}

common/src/main/java/org/opensearch/ml/common/transport/connector/MLCreateConnectorInput.java

+3
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ public MLCreateConnectorInput(
9393
if (protocol == null) {
9494
throw new IllegalArgumentException("Connector protocol is null");
9595
}
96+
if (credential == null || credential.isEmpty()) {
97+
throw new IllegalArgumentException("Connector credential is null or empty list");
98+
}
9699
}
97100
this.name = name;
98101
this.description = description;

common/src/main/java/org/opensearch/ml/common/utils/IndexUtils.java

+84-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55

66
package org.opensearch.ml.common.utils;
77

8+
import static org.opensearch.ml.common.utils.StringUtils.validateSchema;
9+
810
import java.io.IOException;
911
import java.net.URL;
12+
import java.util.HashMap;
1013
import java.util.Map;
1114

1215
import com.google.common.base.Charsets;
@@ -40,20 +43,99 @@ public class IndexUtils {
4043
public static final Map<String, Object> UPDATED_DEFAULT_INDEX_SETTINGS = Map.of("index.auto_expand_replicas", "0-1");
4144
public static final Map<String, Object> UPDATED_ALL_NODES_REPLICA_INDEX_SETTINGS = Map.of("index.auto_expand_replicas", "0-all");
4245

46+
// Schema that validates system index mappings
47+
public static final String MAPPING_SCHEMA_PATH = "index-mappings/schema.json";
48+
49+
// Placeholders to use within the json mapping files
50+
private static final String USER_PLACEHOLDER = "USER_MAPPING_PLACEHOLDER";
51+
private static final String CONNECTOR_PLACEHOLDER = "CONNECTOR_MAPPING_PLACEHOLDER";
52+
public static final Map<String, String> MAPPING_PLACEHOLDERS = Map
53+
.of(USER_PLACEHOLDER, "index-mappings/placeholders/user.json", CONNECTOR_PLACEHOLDER, "index-mappings/placeholders/connector.json");
54+
4355
public static String getMappingFromFile(String path) throws IOException {
4456
URL url = IndexUtils.class.getClassLoader().getResource(path);
4557
if (url == null) {
4658
throw new IOException("Resource not found: " + path);
4759
}
4860

4961
String mapping = Resources.toString(url, Charsets.UTF_8).trim();
50-
if (mapping.isEmpty() || !StringUtils.isJson(mapping)) {
51-
throw new IllegalArgumentException("Invalid or non-JSON mapping at: " + path);
62+
if (mapping.isEmpty()) {
63+
throw new IllegalArgumentException("Empty mapping found at: " + path);
5264
}
5365

66+
mapping = replacePlaceholders(mapping);
67+
validateMapping(mapping);
68+
5469
return mapping;
5570
}
5671

72+
public static String replacePlaceholders(String mapping) throws IOException {
73+
if (mapping == null || mapping.isBlank()) {
74+
throw new IllegalArgumentException("Mapping cannot be null or empty");
75+
}
76+
77+
// Preload resources into memory to avoid redundant I/O
78+
Map<String, String> loadedPlaceholders = new HashMap<>();
79+
for (Map.Entry<String, String> placeholder : MAPPING_PLACEHOLDERS.entrySet()) {
80+
URL url = IndexUtils.class.getClassLoader().getResource(placeholder.getValue());
81+
if (url == null) {
82+
throw new IOException("Resource not found: " + placeholder.getValue());
83+
}
84+
85+
loadedPlaceholders.put(placeholder.getKey(), Resources.toString(url, Charsets.UTF_8));
86+
}
87+
88+
StringBuilder result = new StringBuilder(mapping);
89+
for (Map.Entry<String, String> entry : loadedPlaceholders.entrySet()) {
90+
String placeholder = entry.getKey();
91+
String replacement = entry.getValue();
92+
93+
// Replace all occurrences of the placeholder
94+
int index;
95+
while ((index = result.indexOf(placeholder)) != -1) {
96+
result.replace(index, index + placeholder.length(), replacement);
97+
}
98+
}
99+
100+
return result.toString();
101+
}
102+
103+
/**
104+
* Checks if the provided mapping is a valid JSON and validates it against a schema.
105+
*
106+
* <p>The schema is located at <code>mappings/schema.json</code> and enforces the following validations:</p>
107+
*
108+
* <ul>
109+
* <li>Mandatory fields:
110+
* <ul>
111+
* <li><code>_meta</code></li>
112+
* <li><code>_meta.schema_version</code></li>
113+
* <li><code>properties</code></li>
114+
* </ul>
115+
* </li>
116+
* <li>No additional fields are allowed at the root level.</li>
117+
* <li>No additional fields are allowed in the <code>_meta</code> object.</li>
118+
* <li><code>properties</code> must be an object type.</li>
119+
* <li><code>_meta</code> must be an object type.</li>
120+
* <li><code>_meta.schema_version</code> must be an integer.</li>
121+
* </ul>
122+
*
123+
* <p><strong>Note:</strong> Validation can be made stricter if a specific schema is defined for each index.</p>
124+
*/
125+
public static void validateMapping(String mapping) throws IOException {
126+
if (mapping.isBlank() || !StringUtils.isJson(mapping)) {
127+
throw new IllegalArgumentException("Invalid or non-JSON mapping found: " + mapping);
128+
}
129+
130+
URL url = IndexUtils.class.getClassLoader().getResource(MAPPING_SCHEMA_PATH);
131+
if (url == null) {
132+
throw new IOException("Resource not found: " + MAPPING_SCHEMA_PATH);
133+
}
134+
135+
String schema = Resources.toString(url, Charsets.UTF_8);
136+
validateSchema(schema, mapping);
137+
}
138+
57139
public static Integer getVersionFromMapping(String mapping) {
58140
if (mapping == null || mapping.isBlank()) {
59141
throw new IllegalArgumentException("Mapping cannot be null or empty");

common/src/main/java/org/opensearch/ml/common/utils/ModelInterfaceUtils.java

+3-12
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,7 @@ public class ModelInterfaceUtils {
5252
+ " \"texts\"\n"
5353
+ " ]\n"
5454
+ " }\n"
55-
+ " },\n"
56-
+ " \"required\": [\n"
57-
+ " \"parameters\"\n"
58-
+ " ]\n"
55+
+ " }\n"
5956
+ "}";
6057

6158
private static final String TITAN_TEXT_EMBEDDING_MODEL_INTERFACE_INPUT = "{\n"
@@ -72,10 +69,7 @@ public class ModelInterfaceUtils {
7269
+ " \"inputText\"\n"
7370
+ " ]\n"
7471
+ " }\n"
75-
+ " },\n"
76-
+ " \"required\": [\n"
77-
+ " \"parameters\"\n"
78-
+ " ]\n"
72+
+ " }\n"
7973
+ "}";
8074

8175
private static final String TITAN_MULTI_MODAL_EMBEDDING_MODEL_INTERFACE_INPUT = "{\n"
@@ -92,10 +86,7 @@ public class ModelInterfaceUtils {
9286
+ " }\n"
9387
+ " }\n"
9488
+ " }\n"
95-
+ " },\n"
96-
+ " \"required\": [\n"
97-
+ " \"parameters\"\n"
98-
+ " ]\n"
89+
+ " }\n"
9990
+ "}";
10091

10192
private static final String AMAZON_COMPREHEND_DETECTDOMAINANTLANGUAGE_API_INTERFACE_INPUT = "{\n"

common/src/main/java/org/opensearch/ml/common/utils/StringUtils.java

+35
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,27 @@
1818
import java.util.Set;
1919
import java.util.regex.Matcher;
2020
import java.util.regex.Pattern;
21+
import java.util.stream.Collectors;
2122

2223
import org.apache.commons.lang3.BooleanUtils;
2324
import org.json.JSONArray;
2425
import org.json.JSONException;
2526
import org.json.JSONObject;
27+
import org.opensearch.OpenSearchParseException;
2628

29+
import com.fasterxml.jackson.core.JsonProcessingException;
30+
import com.fasterxml.jackson.databind.JsonNode;
31+
import com.fasterxml.jackson.databind.ObjectMapper;
2732
import com.google.gson.Gson;
2833
import com.google.gson.JsonElement;
2934
import com.google.gson.JsonObject;
3035
import com.google.gson.JsonParser;
3136
import com.google.gson.JsonSyntaxException;
3237
import com.jayway.jsonpath.JsonPath;
38+
import com.networknt.schema.JsonSchema;
39+
import com.networknt.schema.JsonSchemaFactory;
40+
import com.networknt.schema.SpecVersion;
41+
import com.networknt.schema.ValidationMessage;
3342

3443
import lombok.extern.log4j.Log4j2;
3544

@@ -54,6 +63,8 @@ public class StringUtils {
5463
}
5564
public static final String TO_STRING_FUNCTION_NAME = ".toString()";
5665

66+
private static final ObjectMapper MAPPER = new ObjectMapper();
67+
5768
public static boolean isValidJsonString(String json) {
5869
if (json == null || json.isBlank()) {
5970
return false;
@@ -336,4 +347,28 @@ public static JsonObject getJsonObjectFromString(String jsonString) {
336347
return JsonParser.parseString(jsonString).getAsJsonObject();
337348
}
338349

350+
public static void validateSchema(String schemaString, String instanceString) {
351+
try {
352+
// parse the schema JSON as string
353+
JsonNode schemaNode = MAPPER.readTree(schemaString);
354+
JsonSchema schema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012).getSchema(schemaNode);
355+
356+
// JSON data to validate
357+
JsonNode jsonNode = MAPPER.readTree(instanceString);
358+
359+
// Validate JSON node against the schema
360+
Set<ValidationMessage> errors = schema.validate(jsonNode);
361+
if (!errors.isEmpty()) {
362+
String errorMessage = errors.stream().map(ValidationMessage::getMessage).collect(Collectors.joining(", "));
363+
364+
throw new OpenSearchParseException(
365+
"Validation failed: " + errorMessage + " for instance: " + instanceString + " with schema: " + schemaString
366+
);
367+
}
368+
} catch (JsonProcessingException e) {
369+
throw new IllegalArgumentException("Invalid JSON format: " + e.getMessage(), e);
370+
} catch (Exception e) {
371+
throw new OpenSearchParseException("Schema validation failed: " + e.getMessage(), e);
372+
}
373+
}
339374
}

0 commit comments

Comments
 (0)