Skip to content
Snippets Groups Projects
Commit 24c2d584 authored by ml bonhomme's avatar ml bonhomme :bee: Committed by Erwan Rouchet
Browse files

Check user configuration in CreateDockerWorkerVersion

parent 759a472f
No related branches found
No related tags found
1 merge request!2277Check user configuration in CreateDockerWorkerVersion
This commit is part of merge request !2277. Comments created here will be created in the context of that merge request.
......@@ -605,6 +605,7 @@ class DockerWorkerVersionSerializer(serializers.ModelSerializer):
)
gpu_usage = EnumField(FeatureUsage, required=False, default=FeatureUsage.Disabled)
model_usage = EnumField(FeatureUsage, required=False, default=FeatureUsage.Disabled)
configuration = serializers.DictField(required=False, default={})
class Meta:
model = WorkerVersion
......@@ -630,6 +631,20 @@ class DockerWorkerVersionSerializer(serializers.ModelSerializer):
}
}
def validate_configuration(self, configuration):
errors = defaultdict(list)
user_configuration = configuration.get("user_configuration")
if not user_configuration:
return configuration
field = serializers.DictField(child=UserConfigurationFieldSerializer())
try:
field.to_internal_value(user_configuration)
except ValidationError as e:
errors["user_configuration"].append(e.detail)
if errors:
raise ValidationError(errors)
return configuration
@transaction.atomic
def create(self, validated_data):
"""
......
......@@ -6,6 +6,7 @@ from rest_framework import status
from arkindex.ponos.models import Farm
from arkindex.process.models import FeatureUsage, GitRefType, Process, ProcessMode, Repository, Worker, WorkerType
from arkindex.project.tests import FixtureAPITestCase
from arkindex.training.models import Model
from arkindex.users.models import Role, Scope
......@@ -128,6 +129,7 @@ class TestDockerWorkerVersion(FixtureAPITestCase):
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertDictEqual(response.json(), {
"configuration": ['Expected a dictionary of items but got type "str".'],
"docker_image_iid": ["Not a valid string."],
"gpu_usage": ["Value is not of type FeatureUsage"],
"model_usage": ["Value is not of type FeatureUsage"],
......@@ -546,3 +548,600 @@ class TestDockerWorkerVersion(FixtureAPITestCase):
self.assertListEqual(list(new_repo.memberships.values_list("user", "level")), [
(self.user.id, Role.Admin.value)
])
# test version user configuration
def test_create_version_valid_user_configuration(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_integer": {"title": "Demo Integer", "type": "int", "required": True, "default": 1},
"demo_boolean": {"title": "Demo Boolean", "type": "bool", "required": False, "default": True},
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.json()["configuration"], {
"user_configuration": {
"demo_integer": {
"title": "Demo Integer",
"type": "int",
"required": True,
"default": 1
},
"demo_boolean": {
"title": "Demo Boolean",
"type": "bool",
"required": False,
"default": True
}
}
})
def test_create_version_valid_user_configuration_dict(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_dict": {"title": "Demo Dict", "type": "dict", "required": True, "default": {"a": "b", "c": "d"}},
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.json()["configuration"], {
"user_configuration": {
"demo_dict": {
"title": "Demo Dict",
"type": "dict",
"required": True,
"default": {"a": "b", "c": "d"}
}
}
})
def test_create_version_user_configuration_dict_strings_only(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_dict": {"title": "Demo Dict", "type": "dict", "required": True, "default": {"a": ["12", "13"], "c": "d"}},
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_version_valid_user_configuration_enum(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_choice": {"title": "Decisions", "type": "enum", "required": True, "default": 1, "choices": [1, 2, 3]}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.json()["configuration"], {
"user_configuration": {
"demo_choice": {
"title": "Decisions",
"type": "enum",
"required": True,
"default": 1,
"choices": [1, 2, 3]
}
}
})
def test_create_version_valid_user_configuration_list(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_list": {"title": "Demo List", "type": "list", "required": True, "subtype": "int", "default": [1, 2, 3, 4]},
"boolean_list": {"title": "It's a list of booleans", "type": "list", "required": False, "subtype": "bool", "default": [True, False, False]}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.json()["configuration"], {
"user_configuration": {
"demo_list": {
"title": "Demo List",
"type": "list",
"subtype": "int",
"required": True,
"default": [1, 2, 3, 4]
},
"boolean_list": {
"title": "It's a list of booleans",
"type": "list",
"subtype": "bool",
"required": False,
"default": [True, False, False]
}
}
})
def test_create_version_valid_user_configuration_model(self):
test_model = Model.objects.create(
name="Generic model",
public=False,
)
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_model": {"title": "Model for training", "type": "model", "required": True},
"other_model": {"title": "Model the second", "type": "model", "default": str(test_model.id)}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.json()["configuration"], {
"user_configuration": {
"demo_model": {
"title": "Model for training",
"type": "model",
"required": True
},
"other_model": {
"title": "Model the second",
"type": "model",
"default": str(test_model.id)
}
}
})
def test_create_version_invalid_user_configuration_list_requires_subtype(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_list": {"title": "Demo List", "type": "list", "required": True, "default": [1, 2, 3, 4]},
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"demo_list": {
"subtype": ['The "subtype" field must be set for "list" type properties.']
}
}]
}
})
def test_create_version_invalid_user_configuration_list_wrong_default(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_list": {"title": "Demo List", "type": "list", "required": True, "subtype": "int", "default": 12},
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"demo_list": {
"default": ["This is not a valid value for a field of type list."]
}
}]
}
})
def test_create_version_invalid_user_configuration_list_wrong_subtype(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_list": {"title": "Demo List", "type": "list", "required": True, "subtype": "dict", "default": [1, 2, 3, 4]},
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"demo_list": {
"subtype": ["Subtype can only be int, float, bool or string."]
}
}]
}
})
def test_create_version_invalid_user_configuration_list_wrong_default_subtype(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_list": {"title": "Demo List", "type": "list", "required": True, "subtype": "int", "default": [1, 2, "three", 4]},
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"demo_list": {
"default": ["All items in the default value must be of type int."]
}
}]
}
})
def test_create_version_invalid_user_configuration_not_list_choices(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_choice": {"title": "Decisions", "type": "enum", "required": True, "default": 1, "choices": "eeee"}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"demo_choice": {
"choices": ['Expected a list of items but got type "str".']
}
}]
}
})
def test_create_version_invalid_user_configuration_not_dict(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": "non"
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {"configuration": {"user_configuration": [['Expected a dictionary of items but got type "str".']]}})
def test_create_version_invalid_user_configuration_item_not_dict(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"secrets": ["aaaaaa"]
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {"configuration": {
"user_configuration": [{
"secrets":
{"__all__": ["User configuration field definitions should be of type dict, not list."]}
}]
}})
def test_create_version_invalid_user_configuration_wrong_field_type(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"something": {
"title": "some thing",
"type": "uh oh",
"required": 2
}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"something": {
"required": ["Must be a valid boolean."],
"type": ["Value is not of type UserConfigurationFieldType"]
}
}]
}
})
def test_create_version_invalid_user_configuration_wrong_default_type(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"one_float": {
"title": "a float",
"type": "float",
"default": "bonjour",
"required": True
}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"one_float": {
"default": ["This is not a valid value for a field of type float."]
}
}]
}
})
def test_create_version_invalid_user_configuration_choices_no_enum(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"something": {
"title": "some thing",
"type": "int",
"required": False,
"choices": [1, 2, 3]
}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"something": {
"choices": ['The "choices" field can only be set for an "enum" type property.']
}
}]
}
})
def test_create_version_invalid_user_configuration_missing_key(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_integer": {"type": "int", "required": True, "default": 1}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.json(),
{
"configuration": {
"user_configuration": [{
"demo_integer": {
"title": ["This field is required."]
}
}]
}
}
)
def test_create_version_invalid_user_configuration_invalid_key(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_integer": {
"title": "an integer",
"type": "int",
"required": True,
"default": 1,
"some_key": "oh no",
}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [
{
"demo_integer": {
"some_key": ["Configurable properties can only be defined using the following keys: title, type, required, default, subtype, choices."]
}
}
]
}
})
def test_create_version_invalid_user_configuration_default_value(self):
self.client.force_login(self.user)
cases = [
({"type": "int", "default": False}, "int"),
({"type": "int", "default": True}, "int"),
({"type": "float", "default": False}, "float"),
({"type": "float", "default": True}, "float"),
({"type": "bool", "default": 0}, "bool"),
({"type": "bool", "default": 1}, "bool"),
({"type": "string", "default": 1}, "string"),
({"type": "dict", "default": ["a", "b"]}, "dict"),
({"type": "model", "default": "gigi hadid"}, "model"),
({"type": "model", "default": False}, "model")
]
for params, expected in cases:
with self.subTest(**params):
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"param": {"title": "param", **params}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {"configuration": {"user_configuration": [{"param": {"default": [f"This is not a valid value for a field of type {expected}."]}}]}})
def test_create_version_user_configuration_model_default_doesnt_exist(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"param": {"title": "Model to train", "type": "model", "default": "12341234-1234-1234-1234-123412341234"}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {"configuration": {"user_configuration": [{"param": {"default": [
"Model 12341234-1234-1234-1234-123412341234 not found."
]}}]}})
......@@ -1552,7 +1552,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
},
})
def test_create_version_empty_configuration(self):
def test_create_version_configuration_wrong_type(self):
"""
Configuration body must be an object
"""
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment