-
Notifications
You must be signed in to change notification settings - Fork 138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature/multi_tenancy] Tenant-aware integration tests for Connector, Model, Agent, Model Groups #2818
Open
dbwiddis
wants to merge
36
commits into
opensearch-project:feature/multi_tenancy
Choose a base branch
from
dbwiddis:integ-test
base: feature/multi_tenancy
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
[Feature/multi_tenancy] Tenant-aware integration tests for Connector, Model, Agent, Model Groups #2818
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
4b61d13
Tenant-aware integration tests for Connector
dbwiddis 341d2bc
Updated Remote Client creation, working remote connections
dbwiddis 9ef8e31
Refactor repeated method calls into abstract parent class
dbwiddis 7701954
Add Model CRUD Tenant Aware IT
dbwiddis 988bc15
Additional simplification of abstract class
dbwiddis fd57a0f
Add tests for Agent
dbwiddis c3e7db2
Execute Agent Integ test
dbwiddis 325312b
Handle OpenSearchStatusException error details
dbwiddis 01a27cc
Handle Agent Execute with incorrect api key
dbwiddis 3f74030
Exclude the document just deleted from the model delete by query
dbwiddis fee996f
Add Tenant Aware Integ Tests for Model Group
dbwiddis 737b8b5
Delay context restoring for initializing master key and deleting agent
dbwiddis 2e9b22f
Remote tests working
dbwiddis 5f68e18
Working tests after rebasing with tenant-aware search and mappings
dbwiddis f581923
Fix Connector Update, Search, and Delete tenant awareness
dbwiddis 1e6088a
Make model group access control checks tenant aware
dbwiddis 4f5d916
Control multitenancy from system property
dbwiddis b7f5f02
Add tenant-aware tests to CI build matrix
dbwiddis d846328
Fix mapping and parsing issues on remote client
dbwiddis dd375b4
Update tests for remote client
dbwiddis 314a12e
Field rename is better fix for version typing bug
dbwiddis 0c4a6e3
Properly parse ToXContentObjects into JSON-P without ObjectMapper
dbwiddis 151274b
Ensure config index deletion is processed at start of test
dbwiddis a7a701b
Fix SdkClient credential handling for AWS endpoints
dbwiddis 170a57c
Use assertBusy for searching
dbwiddis ea76b8c
Implement retryOnConflict on UpdateDataObject
dbwiddis e343dab
All integ tests working
dbwiddis 798b3bb
Add more missing tenant ids in requests
dbwiddis f501e35
Fetch existing object and only update changed fields
dbwiddis 117b32a
Revert inadvertent migration of deleteController to SdkClient
dbwiddis 01f9c5e
Simplify restoring context with runBefore
dbwiddis 463dadb
Refactor to move some objects out of the retry loop
dbwiddis 0d8173c
Make tenant awareness setting static
dbwiddis c6d070b
Update build.gradle to actually check tenantaware property value
dbwiddis 7eabdd5
Cleanup other resources created as part of Integ Tests
dbwiddis b2decab
Add integration test covering bulk API
dbwiddis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
243 changes: 243 additions & 0 deletions
243
plugin/src/test/java/org/opensearch/ml/rest/MLCommonsTenantAwareRestTestCase.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.ml.rest; | ||
|
||
import static java.nio.charset.StandardCharsets.UTF_8; | ||
import static java.util.Collections.emptyMap; | ||
import static java.util.Collections.singletonList; | ||
import static org.opensearch.common.xcontent.XContentType.JSON; | ||
import static org.opensearch.ml.common.input.Constants.TENANT_ID_HEADER; | ||
import static org.opensearch.ml.settings.MLCommonsSettings.ML_COMMONS_MULTI_TENANCY_ENABLED; | ||
|
||
import java.io.IOException; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.stream.Collectors; | ||
|
||
import org.apache.http.Header; | ||
import org.apache.http.message.BasicHeader; | ||
import org.opensearch.action.search.SearchResponse; | ||
import org.opensearch.client.Response; | ||
import org.opensearch.client.ResponseException; | ||
import org.opensearch.common.xcontent.json.JsonXContent; | ||
import org.opensearch.core.common.bytes.BytesArray; | ||
import org.opensearch.core.rest.RestStatus; | ||
import org.opensearch.core.xcontent.DeprecationHandler; | ||
import org.opensearch.core.xcontent.NamedXContentRegistry; | ||
import org.opensearch.core.xcontent.XContentParser; | ||
import org.opensearch.ml.common.input.Constants; | ||
import org.opensearch.ml.utils.TestHelper; | ||
import org.opensearch.rest.RestRequest; | ||
import org.opensearch.test.rest.FakeRestRequest; | ||
|
||
public abstract class MLCommonsTenantAwareRestTestCase extends MLCommonsRestTestCase { | ||
|
||
// Toggle to run DDB tests | ||
// TODO: Get this from a property | ||
protected static final boolean DDB = false; | ||
|
||
protected static final String DOC_ID = "_id"; | ||
|
||
// REST methods | ||
protected static final String POST = RestRequest.Method.POST.name(); | ||
protected static final String GET = RestRequest.Method.GET.name(); | ||
protected static final String PUT = RestRequest.Method.PUT.name(); | ||
protected static final String DELETE = RestRequest.Method.DELETE.name(); | ||
|
||
// REST paths; some subclasses need multiple of these | ||
protected static final String AGENTS_PATH = "/_plugins/_ml/agents/"; | ||
protected static final String CONNECTORS_PATH = "/_plugins/_ml/connectors/"; | ||
protected static final String MODELS_PATH = "/_plugins/_ml/models/"; | ||
protected static final String MODEL_GROUPS_PATH = "/_plugins/_ml/model_groups/"; | ||
|
||
// REST body | ||
protected static final String MATCH_ALL_QUERY = "{\"query\":{\"match_all\":{}}}"; | ||
protected static final String EMPTY_CONTENT = "{}"; | ||
|
||
// REST Response error reasons | ||
protected static final String MISSING_TENANT_REASON = "Tenant ID header is missing"; | ||
protected static final String NO_PERMISSION_REASON = "You don't have permission to access this resource"; | ||
protected static final String DEPLOYED_REASON = | ||
"Model cannot be deleted in deploying or deployed state. Try undeploy model first then delete"; | ||
|
||
// Common constants and fields used in subclasses | ||
protected static final String CONNECTOR_ID = "connector_id"; | ||
|
||
protected String tenantId = randomAlphaOfLength(5); | ||
protected String otherTenantId = randomAlphaOfLength(6); | ||
|
||
protected final RestRequest tenantRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY) | ||
.withHeaders(Map.of(TENANT_ID_HEADER, singletonList(tenantId))) | ||
.build(); | ||
protected final RestRequest otherTenantRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY) | ||
.withHeaders(Map.of(TENANT_ID_HEADER, singletonList(otherTenantId))) | ||
.build(); | ||
protected final RestRequest nullTenantRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY) | ||
.withHeaders(emptyMap()) | ||
.build(); | ||
|
||
protected final RestRequest tenantMatchAllRequest = getRestRequestWithHeadersAndContent(tenantId, MATCH_ALL_QUERY); | ||
protected final RestRequest otherTenantMatchAllRequest = getRestRequestWithHeadersAndContent(otherTenantId, MATCH_ALL_QUERY); | ||
protected final RestRequest nullTenantMatchAllRequest = getRestRequestWithHeadersAndContent(null, MATCH_ALL_QUERY); | ||
|
||
protected static boolean isMultiTenancyEnabled() throws IOException { | ||
// pass -Dtests.rest.tenantaware=true on gradle command line to enable | ||
return Boolean.parseBoolean(System.getProperty(ML_COMMONS_MULTI_TENANCY_ENABLED.getKey())) | ||
|| Boolean.parseBoolean(System.getenv(ML_COMMONS_MULTI_TENANCY_ENABLED.getKey())); | ||
} | ||
|
||
protected static Response makeRequest(RestRequest request, String method, String path) throws IOException { | ||
return TestHelper | ||
.makeRequest(client(), method, path, request.params(), request.content().utf8ToString(), getHeadersFromRequest(request)); | ||
} | ||
|
||
private static List<Header> getHeadersFromRequest(RestRequest request) { | ||
return request | ||
.getHeaders() | ||
.entrySet() | ||
.stream() | ||
.map(e -> new BasicHeader(e.getKey(), e.getValue().stream().collect(Collectors.joining(",")))) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
protected static RestRequest getRestRequestWithHeadersAndContent(String tenantId, String requestContent) { | ||
Map<String, List<String>> headers = new HashMap<>(); | ||
if (tenantId != null) { | ||
headers.put(Constants.TENANT_ID_HEADER, singletonList(tenantId)); | ||
} | ||
return new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY) | ||
.withHeaders(headers) | ||
.withContent(new BytesArray(requestContent), JSON) | ||
.build(); | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
protected static Map<String, Object> responseToMap(Response response) throws IOException { | ||
return parseResponseToMap(response); | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
protected static String getErrorReasonFromResponseMap(Map<String, Object> map) { | ||
// Two possible cases: | ||
String type = ((Map<String, String>) map.get("error")).get("type"); | ||
|
||
// { | ||
// "error": { | ||
// "root_cause": [ | ||
// { | ||
// "type": "status_exception", | ||
// "reason": "You don't have permission to access this resource" | ||
// } | ||
// ], | ||
// "type": "status_exception", | ||
// "reason": "You don't have permission to access this resource" | ||
// }, | ||
// "status": 403 | ||
// } | ||
if ("status_exception".equals(type)) { | ||
return ((Map<String, String>) map.get("error")).get("reason"); | ||
} | ||
|
||
// Due to https://github.com/opensearch-project/ml-commons/issues/2958 | ||
if ("m_l_resource_not_found_exception".equals(type)) { | ||
return ((Map<String, String>) map.get("error")).get("reason"); | ||
} | ||
|
||
// { | ||
// "error": { | ||
// "reason": "System Error", | ||
// "details": "You don't have permission to access this resource", | ||
// "type": "OpenSearchStatusException" | ||
// }, | ||
// "status": 403 | ||
// } | ||
return ((Map<String, String>) map.get("error")).get("details"); | ||
} | ||
|
||
protected static SearchResponse searchResponseFromResponse(Response response) throws IOException { | ||
XContentParser parser = JsonXContent.jsonXContent | ||
.createParser( | ||
NamedXContentRegistry.EMPTY, | ||
DeprecationHandler.IGNORE_DEPRECATIONS, | ||
TestHelper.httpEntityToString(response.getEntity()).getBytes(UTF_8) | ||
); | ||
return SearchResponse.fromXContent(parser); | ||
} | ||
|
||
protected static void assertBadRequest(Response response) { | ||
assertEquals(RestStatus.BAD_REQUEST.getStatus(), response.getStatusLine().getStatusCode()); | ||
} | ||
|
||
protected static void assertNotFound(Response response) { | ||
assertEquals(RestStatus.NOT_FOUND.getStatus(), response.getStatusLine().getStatusCode()); | ||
} | ||
|
||
protected static void assertForbidden(Response response) { | ||
assertEquals(RestStatus.FORBIDDEN.getStatus(), response.getStatusLine().getStatusCode()); | ||
} | ||
|
||
protected static void assertUnauthorized(Response response) { | ||
assertEquals(RestStatus.UNAUTHORIZED.getStatus(), response.getStatusLine().getStatusCode()); | ||
} | ||
|
||
protected void refreshBeforeSearch(boolean extraDelay) { | ||
try { | ||
refreshAllIndices(); | ||
Thread.sleep(extraDelay ? 60000L : 5000L); | ||
} catch (IOException | InterruptedException e) { | ||
// ignore | ||
} | ||
} | ||
|
||
/** | ||
* Delete the specified document and wait until a search matches only the specified number of hits | ||
* @param tenantId The tenant ID to filter the search by | ||
* @param restPath The base path for the REST API | ||
* @param id The document ID to be appended to the REST API for deletion | ||
* @param hits The number of hits to expect after the deletion is processed | ||
* @throws Exception on failures with building or making the request | ||
*/ | ||
protected static void deleteAndWaitForSearch(String tenantId, String restPath, String id, int hits) throws Exception { | ||
RestRequest request = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY) | ||
.withHeaders(Map.of(TENANT_ID_HEADER, singletonList(tenantId))) | ||
.build(); | ||
// First process the deletion. Dependent resources (e.g. model with connector) may cause 409 status until they are deleted | ||
assertBusy(() -> { | ||
try { | ||
Response deleteResponse = makeRequest(request, DELETE, restPath + id); | ||
// first successful deletion should produce an OK | ||
assertOK(deleteResponse); | ||
} catch (ResponseException e) { | ||
// repeat deletions can produce a 404, treat as a success | ||
assertNotFound(e.getResponse()); | ||
} | ||
}, 20, TimeUnit.SECONDS); | ||
// Deletion processed, now wait for it to disappear from search | ||
RestRequest searchRequest = getRestRequestWithHeadersAndContent(tenantId, MATCH_ALL_QUERY); | ||
assertBusy(() -> { | ||
Response response = makeRequest(searchRequest, GET, restPath + "_search"); | ||
assertOK(response); | ||
SearchResponse searchResponse = searchResponseFromResponse(response); | ||
assertEquals(hits, searchResponse.getHits().getTotalHits().value); | ||
}, 20, TimeUnit.SECONDS); | ||
} | ||
|
||
protected static String registerRemoteModelContent(String description, String connectorId, String modelGroupId) { | ||
StringBuilder sb = new StringBuilder(); | ||
sb.append("{\n"); | ||
sb.append(" \"name\": \"remote model for connector_id ").append(connectorId).append("\",\n"); | ||
sb.append(" \"function_name\": \"remote\",\n"); | ||
sb.append(" \"description\": \"").append(description).append("\",\n"); | ||
if (modelGroupId != null) { | ||
sb.append(" \"model_group_id\": \"").append(modelGroupId).append("\",\n"); | ||
} | ||
sb.append(" \"connector_id\": \"").append(connectorId).append("\"\n"); | ||
sb.append("}"); | ||
return sb.toString(); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why it's showing 2 times?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know from where are we getting this
status_exception
? May be we can change that toOpenSearchStatusException
too? I can make the changes.