Skip to content
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

#289 - Add register, update and delete model group functionality to support Model Access Control #332

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a2ffe54
init
rawwar Nov 3, 2023
7bef831
add search, delete and update
rawwar Nov 4, 2023
61c3090
Merge branch 'main' of https://github.com/opensearch-project/opensear…
rawwar Nov 4, 2023
a89bb39
add tests for register model group
rawwar Nov 4, 2023
3bbea93
update cluster to 2.11
rawwar Nov 4, 2023
ca765ec
test skipif
rawwar Nov 4, 2023
4c6a7fd
fix
rawwar Nov 4, 2023
af60958
add tests
rawwar Nov 4, 2023
cde4e1f
update matrix
rawwar Nov 4, 2023
28040ee
cancel in progress
rawwar Nov 4, 2023
d8fc9b2
update concurrency
rawwar Nov 4, 2023
cda4c08
job level concurrency
rawwar Nov 4, 2023
1ebeb1a
fix
rawwar Nov 4, 2023
1b72089
fix
rawwar Nov 4, 2023
e27daa3
fix tests
rawwar Nov 4, 2023
7edafe9
tests passing
rawwar Nov 4, 2023
2e0f9c4
isort fix
rawwar Nov 4, 2023
f9bc8c8
fix action
rawwar Nov 4, 2023
24ea966
fix action
rawwar Nov 4, 2023
84848e2
fix action
rawwar Nov 4, 2023
08256f6
fix
rawwar Nov 4, 2023
9547db3
update changelog
rawwar Nov 4, 2023
06f097d
fix os dockerfile
rawwar Nov 4, 2023
594baad
test
rawwar Nov 5, 2023
203aa86
pass opensearch version
rawwar Nov 5, 2023
19fdf80
fix
rawwar Nov 5, 2023
6bdcfcd
fix
rawwar Nov 5, 2023
76fcd84
fix
rawwar Nov 5, 2023
50bacd4
update OS dockerfile
rawwar Nov 5, 2023
b5133b9
fix failing tests
rawwar Nov 5, 2023
2b529a3
update dockerfile for 2.11.0
rawwar Nov 5, 2023
b61c1e3
remove disable warning
rawwar Nov 6, 2023
d5d5830
fix upload model
rawwar Nov 6, 2023
8f46cf9
fix lint
rawwar Nov 6, 2023
d2f832e
fix lint
rawwar Nov 6, 2023
286716e
Merge branch 'main' of https://github.com/opensearch-project/opensear…
rawwar Nov 6, 2023
24cb85c
include reference
rawwar Nov 6, 2023
078443f
pr fixes
rawwar Nov 6, 2023
c5cbde6
lint fix
rawwar Nov 6, 2023
c8dd748
fix lint
rawwar Nov 6, 2023
05a42b2
fix tests
rawwar Nov 6, 2023
4e47809
skip
rawwar Nov 6, 2023
bdf1201
fix lint
rawwar Nov 6, 2023
df6c069
fix lint and increase coverage
rawwar Nov 6, 2023
62b5601
fix lint
rawwar Nov 6, 2023
1993945
fix
rawwar Nov 6, 2023
2c6b346
feedback fixes
rawwar Nov 6, 2023
3b16fed
fix
rawwar Nov 9, 2023
d8479ff
lint fix
rawwar Nov 9, 2023
99a3660
fix test cases
rawwar Nov 9, 2023
ba60da7
pr feedback fixes
rawwar Nov 13, 2023
075a763
revert
rawwar Nov 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 9 additions & 21 deletions opensearch_py_ml/ml_commons/model_access_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
validate_create_model_group_parameters,
validate_update_model_group_parameters,
validate_delete_model_group_parameters,
validate_search_model_group_parameters
)


Expand Down Expand Up @@ -49,31 +50,18 @@ def register_model_group(

def update_model_group(
self,
name: str,
description: str,
access_mode: str,
backend_roles: List[str],
add_all_backend_roles=False,
update_query: dict,
model_group_id: Optional[str]=None,
model_group_name: Optional[str]=None,
):
validate_update_model_group_parameters(
name, description, access_mode, backend_roles, add_all_backend_roles
)
body = {"name": name, "add_all_backend_roles": add_all_backend_roles}
if description:
body["description"] = description
if access_mode:
body["access_mode"] = access_mode
if backend_roles:
body["backend_roles"] = backend_roles


validate_update_model_group_parameters(update_query, model_group_id, model_group_name)
return self.client.transport.perform_request(
method="PUT", url=f"{ML_BASE_URI}/{self.API_ENDPOINT}/_update", body=body
method="PUT", url=f"{ML_BASE_URI}/{self.API_ENDPOINT}/", body=update_query
)

def search_model_group(self, query):
if isinstance(query, str):
query = json.loads(query)

def search_model_group(self, query: dict):
validate_search_model_group_parameters(query)
return self.client.transport.perform_request(
method="GET", url=f"{ML_BASE_URI}/{self.API_ENDPOINT}/_search", body=query
)
Expand Down
36 changes: 26 additions & 10 deletions opensearch_py_ml/ml_commons/validators/model_access_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ def _validate_model_group_add_all_backend_roles(add_all_backend_roles):
if not isinstance(add_all_backend_roles, bool):
raise ValueError("add_all_backend_roles should be a boolean")

def _validate_model_group_query(query, operation=None):
if not isinstance(query, dict):
raise ValueError("query needs to be a dictionary")

if operation and not isinstance(operation, str):
raise ValueError("operation needs to be a string")



def validate_create_model_group_parameters(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason we are not validating all the exception cases while creating model group? Like the ones we have here: https://github.com/opensearch-project/ml-commons/blob/6efb1ebeefb8a7930f6767262457d0d5b568ac75/plugin/src/main/java/org/opensearch/ml/model/MLModelGroupManager.java#L148

Copy link
Contributor Author

@rawwar rawwar Nov 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rbhavna , since, these validations are already being done by ml-commons, having them here as well felt like double validations. Most checks I wrote here just check for type issues and helps to make a valid API call.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, the three exception cases that we are testing here are also being validated by ml-commons. Any reason we still have them here and not the others?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I do feel we shouldn't be having these checks in opensearch-py-ml. Because, if checks on ml-commons change, we again need to update this on the client as well. I was thinking, client libraries only purpose would be to make the calls and if any errors from the opensearch, it should be able to pass them to user without any change. Also, maybe add few utility functions to make things easier for the user.

@dhrubo-os , @rbhavna , what do you think about removing these validations form opensearch-py-ml? Or Should I go ahead with implementing the validations from ml-commons ?

Copy link
Collaborator

@dhrubo-os dhrubo-os Nov 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As opensearch-py-ml is a client plugin for ml-commons, we should have only the client side testing similar like any web application. Some basic input validations which can be easily caught before the request reaching to the server. We don't need to add that type of validations for which we need to make a back-end call like testing if the user is admin or not.

Yeah we need to have the same basic validations in ml-commons too, as py-ml isn't the only client for ml-commons.

name,
rawwar marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -68,16 +76,20 @@ def validate_create_model_group_parameters(
)


def validate_update_model_group_parameters(
name,
description,
access_mode,
backend_roles,
add_all_backend_roles,
):
validate_create_model_group_parameters(
name, description, access_mode, backend_roles, add_all_backend_roles
)
def validate_update_model_group_parameters(update_query, model_group_id, model_group_name):
if model_group_id and model_group_name:
raise ValueError(
"You cannot specify both model_group_id and model_group_name at the same time"
)

if not isinstance(model_group_id, (NoneType, str)):
raise ValueError("Invalid model_group_id. model_group_id needs to be a string")

if not isinstance(model_group_name, (NoneType, str)):
raise ValueError("Invalid model_group_name. model_group_name needs to be a string")

if not isinstance(update_query, dict):
raise ValueError("Invalid update_query. update_query needs to be a dictionary")


def validate_delete_model_group_parameters(model_group_id, model_group_name):
Expand All @@ -91,3 +103,7 @@ def validate_delete_model_group_parameters(model_group_id, model_group_name):

if not isinstance(model_group_name, (NoneType, str)):
raise ValueError("Invalid model_group_name. model_group_name needs to be a string")


def validate_search_model_group_parameters(query):
_validate_model_group_query(query)
131 changes: 115 additions & 16 deletions tests/ml_commons/test_model_access_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@
from opensearch_py_ml.ml_commons.model_access_control import ModelAccessControl
from tests import OPENSEARCH_TEST_CLIENT
from opensearchpy.exceptions import RequestError
from packaging.version import parse as parse_version
import os

OPENSEARCH_VERSION = parse_version(os.environ['OPENSEARCH_VERSION'])
MAC_MIN_VERSION = parse_version("2.8.0")
rawwar marked this conversation as resolved.
Show resolved Hide resolved
MAC_UPDATE_MIN_VERSION = parse_version("2.11.0")
rawwar marked this conversation as resolved.
Show resolved Hide resolved

OPENSEARCH_VERSION = OPENSEARCH_TEST_CLIENT.info()['version']['number']
@pytest.fixture
def client():
return ModelAccessControl(OPENSEARCH_TEST_CLIENT)
Expand All @@ -21,7 +26,7 @@ def client():
def test_model_group(client):
rawwar marked this conversation as resolved.
Show resolved Hide resolved
model_group_name = "__test__model_group_1"
client.delete_model_group(model_group_name=model_group_name)
# time.sleep(0.5)
time.sleep(0.5)
client.register_model_group(
name=model_group_name,
description="test model group for opensearch-py-ml test cases",
Expand All @@ -31,6 +36,8 @@ def test_model_group(client):
client.delete_model_group(model_group_name=model_group_name)



@pytest.mark.skipif(OPENSEARCH_VERSION < MAC_MIN_VERSION, reason="Model groups are supported in OpenSearch 2.8.0 and above")
def test_register_model_group(client):

model_group_name1 = "__test__model_group_A"
Expand Down Expand Up @@ -80,24 +87,116 @@ def test_register_model_group(client):
client.register_model_group(name=model_group_name2)
assert exec_info.value.status_code == 400
assert exec_info.match("The name you provided is already being used by a model group")


@pytest.mark.skipif(OPENSEARCH_VERSION < MAC_UPDATE_MIN_VERSION, reason="Model groups updates are supported in OpenSearch 2.11.0 and above")
def test_update_model_group(client, test_model_group):
# update model group name and description
update_query = {
"name": "__test__model_group_X",
"description": "updated description",
}
update_successful = False
try:
res = client.update_model_group(update_query, model_group_name=test_model_group)
assert isinstance(res, dict)
assert "status" in res
assert res['status'] == "Updated"
update_successful = True
except Exception as ex:
assert False,f"Failed to search model group due to unhandled error: {ex}"

for each in "ABC":
client.delete_model_group(model_group_name=f"__test__model_group_{each}")
if update_successful:
# revert model group name
update_query = {"name": test_model_group}
res = client.update_model_group(update_query)
assert isinstance(res, dict)
assert "status" in res
assert res['status'] == "Updated"



def test_update_model_group():
import os
env_data = os.environ
print("Environ data = ", env_data)
assert False, "!@#"

@pytest.mark.skipif(OPENSEARCH_VERSION >= "2.8.0")
def test_delete_model_group(client):
assert False

def test_search_model_group(client):
pass
@pytest.mark.skipif(OPENSEARCH_VERSION < MAC_MIN_VERSION, reason="Model groups are supported in OpenSearch 2.8.0 and above")
def test_search_model_group(client, test_model_group):
query1 = {"query": {"match": {"name": test_model_group}}, "size": 1}

try:
res = client.search_model_group(query1)
assert isinstance(res, dict)
assert "hits" in res and "hits" in res["hits"]
assert len(res['hits']['hits']) == 1
assert "_source" in res['hits']['hits']
assert "name" in res['hits']['hits']['_source']
assert test_model_group == res['hits']['hits']['_source']['name']
except Exception as ex:
assert False,f"Failed to search model group due to unhandled error: {ex}"

query2 = {"query": {"match": {"name": "test-unknown"}}, "size": 1}
try:
res = client.search_model_group(query2)
assert isinstance(res, dict)
assert "hits" in res and "hits" in res["hits"]
assert len(res['hits']['hits']) == 0
except Exception as ex:
assert False,f"Failed to search model group due to unhandled error: {ex}"

@pytest.mark.skipif(OPENSEARCH_VERSION < MAC_MIN_VERSION, reason="Model groups are supported in OpenSearch 2.8.0 and above")
def test_search_model_group_by_name(client, test_model_group):

try:
res = client.search_model_group_by_name(model_group_name=test_model_group)
assert isinstance(res, dict)
assert "hits" in res and "hits" in res["hits"]
assert len(res['hits']['hits']) == 1
assert "_source" in res['hits']['hits']
assert len(res['hits']['hits']['_source']) > 1
assert "name" in res['hits']['hits']['_source']
assert test_model_group == res['hits']['hits']['_source']['name']
except Exception as ex:
assert False,f"Failed to search model group due to unhandled error: {ex}"


try:
res = client.search_model_group_by_name(model_group_name=test_model_group, _source="name")
assert isinstance(res, dict)
assert "hits" in res and "hits" in res["hits"]
assert len(res['hits']['hits']) == 1
assert "_source" in res['hits']['hits']
assert len(res['hits']['hits']['_source']) == 1
assert "name" in res['hits']['hits']['_source']
except Exception as ex:
assert False,f"Failed to search model group due to unhandled error: {ex}"

try:
res = client.search_model_group_by_name(model_group_name="test-unknown")
assert isinstance(res, dict)
assert "hits" in res and "hits" in res["hits"]
assert len(res['hits']['hits']) == 0
except Exception as ex:
assert False,f"Failed to search model group due to unhandled error: {ex}"


def test_search_model_group_by_name(client):
pass

@pytest.mark.skipif(OPENSEARCH_VERSION < MAC_MIN_VERSION, reason="Model groups are supported in OpenSearch 2.8.0 and above")
def test_delete_model_group(client, test_model_group):
# create a test model group

for each in "AB":
model_group_name = f"__test__model_group_{each}"
res = client.delete_model_group(model_group_name=model_group_name)
assert res is None or isinstance(res, dict)
if isinstance(res, dict):
assert 'result' in res
assert res['result'] in ['not_found', 'deleted']

res = client.delete_model_group(model_group_id="test-unknown")
assert isinstance(res, dict)
assert "result" in res
assert res['result'] == 'not_found'

res = client.delete_model_group(model_group_name=test_model_group)
assert isinstance(res, dict)
assert "result" in res
assert res['result'] == 'deleted'
Loading