diff --git a/arkindex/process/serializers/workers.py b/arkindex/process/serializers/workers.py index 6a00e11b79a3ece24b90f75a001c0aab4b09e0fc..7581e94fecd6a876fd47529cec991d02f9fe69d1 100644 --- a/arkindex/process/serializers/workers.py +++ b/arkindex/process/serializers/workers.py @@ -591,10 +591,24 @@ class DockerWorkerVersionSerializer(serializers.ModelSerializer): worker_slug = serializers.CharField( max_length=100, help_text=dedent(""" - The slug/name of the worker to which a new version will be published. + The slug of the worker to which a new version will be published. If such a worker does not exist, it will be created. """), ) + worker_name = serializers.CharField( + max_length=100, + help_text="The name of the worker to which a new version will be published.", + ) + worker_type = serializers.CharField( + max_length=100, + help_text="The slug of the worker type of the worker to which a new version will be published.", + ) + worker_description = serializers.CharField( + required=False, + allow_null=True, + allow_blank=True, + style={"base_template": "textarea.html"}, + ) revision_hash = serializers.CharField(max_length=50) revision_message = serializers.CharField(required=False, default="created from docker image") revision_author = serializers.CharField(max_length=50, required=False, default="default") @@ -617,6 +631,9 @@ class DockerWorkerVersionSerializer(serializers.ModelSerializer): # Related fields "repository_url", "worker_slug", + "worker_name", + "worker_type", + "worker_description", "revision_hash", "revision_message", "revision_author", @@ -705,23 +722,33 @@ class DockerWorkerVersionSerializer(serializers.ModelSerializer): unique_fields=["repository", "name"], ) - # Use a specific worker type in case a worker must be created - worker_type, _ = WorkerType.objects.using("default").get_or_create(slug="docker", defaults={"display_name": "Docker"}) + # Retrieve or create the worker type + worker_type, _ = WorkerType.objects.using("default").get_or_create(slug=validated_data["worker_type"], defaults={"display_name": validated_data["worker_type"]}) # Retrieve or create the worker - worker, _ = repository.workers.using("default").get_or_create( + worker, created = repository.workers.using("default").get_or_create( slug=validated_data["worker_slug"], repository=repository, defaults={ - "name": validated_data["worker_slug"], - "type": worker_type, + "name": validated_data["worker_name"], + "type_id": worker_type.id, + "description": validated_data.get("worker_description", "") }, ) if worker.archived: raise ValidationError({"worker_slug": ["This worker is archived."]}) + # Update the worker if required + if not created: + description = validated_data.get("worker_description") + if worker.name != validated_data["worker_name"] or worker.type_id != worker_type.id or (description is not None and worker.description != description): + worker.name = validated_data["worker_name"] + worker.type_id = worker_type.id + if description is not None: + worker.description = description + worker.save() # Finally, create the worker version and mark it as available - # If we are about the return an existing worker version, fetch the required data for the response + # If we are about to return an existing worker version, fetch the required data for the response version, created = ( WorkerVersion .objects diff --git a/arkindex/process/tests/test_docker_worker_version.py b/arkindex/process/tests/test_docker_worker_version.py index bea809bbacc6cf5ad02407964f8e1c85857b9133..8117a9145fc278b4b335af022c23a79b2c3d5eeb 100644 --- a/arkindex/process/tests/test_docker_worker_version.py +++ b/arkindex/process/tests/test_docker_worker_version.py @@ -20,8 +20,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): super().setUpTestData() cls.repo = Repository.objects.get(url="http://my_repo.fake/workers/worker") cls.rev = cls.repo.revisions.get() - cls.worker = Worker.objects.get(slug="reco") - cls.version = cls.worker.versions.get() + cls.worker = Worker.objects.filter(slug="reco").select_related("type").get() + cls.version = cls.worker.versions.select_related("revision").get() cls.user.user_scopes.create(scope=Scope.CreateDockerWorkerVersion) cls.worker_type = WorkerType.objects.create(slug="docker", display_name="Docker") @@ -86,6 +86,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "repository_url": self.repo.url, "revision_hash": "new_revision_hash", "worker_slug": self.worker.slug, + "worker_name": self.worker.name, + "worker_type": self.worker.type.slug, }, format="json", ) @@ -103,6 +105,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "repository_url": ["This field is required."], "revision_hash": ["This field is required."], "worker_slug": ["This field is required."], + "worker_name": ["This field is required."], + "worker_type": ["This field is required."], }, ) @@ -117,6 +121,9 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "repository_url": [], "revision_hash": "", "worker_slug": [], + "worker_name": [], + "worker_type": [], + "worker_description": [], "revision_message": [], "revision_author": [], "revision_references": [{"a": 133}], @@ -142,6 +149,9 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "type": ["This field is required."] }], "worker_slug": ["Not a valid string."], + "worker_name": ["Not a valid string."], + "worker_type": ["Not a valid string."], + "worker_description": ["Not a valid string."], }) def test_create_duplicated(self): @@ -149,7 +159,7 @@ class TestDockerWorkerVersion(FixtureAPITestCase): No worker version can be created with an existing revision hash and worker slug """ self.client.force_login(self.user) - with self.assertNumQueries(14): + with self.assertNumQueries(13): response = self.client.post( reverse("api:version-from-docker"), data={ @@ -157,7 +167,9 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "docker_image_iid": "some_docker_image", "repository_url": self.repo.url, "revision_hash": self.version.revision.hash, - "worker_slug": self.version.worker.slug, + "worker_slug": self.worker.slug, + "worker_name": self.worker.name, + "worker_type": self.worker.type.slug, }, format="json", ) @@ -180,6 +192,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "repository_url": self.repo.url, "revision_hash": "new_revision_hash", "worker_slug": self.worker.slug, + "worker_name": self.worker.name, + "worker_type": self.worker.type.slug, }, format="json", ) @@ -194,6 +208,7 @@ class TestDockerWorkerVersion(FixtureAPITestCase): A new version can be published with only the required fields """ self.client.force_login(self.user) + with self.assertNumQueries(18): response = self.client.post( reverse("api:version-from-docker"), @@ -203,6 +218,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "repository_url": self.repo.url, "revision_hash": "new_revision_hash", "worker_slug": self.worker.slug, + "worker_name": self.worker.name, + "worker_type": self.worker.type.slug, }, format="json", ) @@ -243,6 +260,7 @@ class TestDockerWorkerVersion(FixtureAPITestCase): A new version can be published with the optional values """ self.client.force_login(self.user) + with self.assertNumQueries(19): response = self.client.post( reverse("api:version-from-docker"), @@ -252,6 +270,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "repository_url": self.repo.url, "revision_hash": "new_revision_hash", "worker_slug": self.worker.slug, + "worker_name": self.worker.name, + "worker_type": self.worker.type.slug, "revision_message": "Bruce was very clever", "revision_author": "Iwan Roberts", "revision_references": [ @@ -304,6 +324,79 @@ class TestDockerWorkerVersion(FixtureAPITestCase): # Existing repository memberships are not updated self.assertFalse(self.repo.memberships.exists()) + def test_create_update_worker(self): + """ + Creating a new worker version can update the worker, and create a new worker type + """ + self.client.force_login(self.user) + + with self.assertNumQueries(23): + response = self.client.post( + reverse("api:version-from-docker"), + data={ + "configuration": {"test": "A"}, + "docker_image_iid": "e" * 512, + "repository_url": self.repo.url, + "revision_hash": "new_revision_hash", + "worker_slug": self.worker.slug, + "worker_name": "A New Name", + "worker_type": "new_worker_type", + "worker_description": "C'est un petit val qui mousse de rayons.", + "revision_message": "Bruce was very clever", + "revision_author": "Iwan Roberts", + "revision_references": [ + {"type": "branch", "name": "master"}, + {"type": "tag", "name": "2.0"}, + ], + "gpu_usage": FeatureUsage.Required.value, + "model_usage": FeatureUsage.Supported.value, + }, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + new_worker_type = WorkerType.objects.get(slug="new_worker_type") + new_revision = self.repo.revisions.get(hash="new_revision_hash") + self.worker.refresh_from_db() + self.assertEqual(self.worker.description, "C'est un petit val qui mousse de rayons.") + refs = list(new_revision.refs.all()) + self.assertDictEqual( + {ref.type: ref.name for ref in refs}, + {GitRefType.Branch: "master", GitRefType.Tag: "2.0"}, + ) + new_version = new_revision.versions.get() + self.assertDictEqual(response.json(), { + "id": str(new_version.id), + "configuration": {"test": "A"}, + "docker_image": None, + "docker_image_iid": "e" * 512, + "docker_image_name": new_version.docker_image_name, + "gpu_usage": FeatureUsage.Required.value, + "model_usage": FeatureUsage.Supported.value, + "revision": { + "id": str(new_revision.id), + "author": "Iwan Roberts", + "commit_url": "http://my_repo.fake/workers/worker/commit/new_revision_hash", + "created": new_revision.created.isoformat().replace("+00:00", "Z"), + "hash": "new_revision_hash", + "message": "Bruce was very clever", + "refs": [ + {"id": str(ref.id), "name": ref.name, "type": ref.type.value} + for ref in refs + ], + }, + "version": None, + "created": new_version.created.isoformat().replace("+00:00", "Z"), + "state": "available", + "worker": { + "id": str(self.worker.id), + "name": "A New Name", + "slug": "reco", + "type": new_worker_type.slug + } + }) + # Existing repository memberships are not updated + self.assertFalse(self.repo.memberships.exists()) + def test_create_update_git_ref(self): """ Existing GitRefs on the repository on a different revision @@ -331,6 +424,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "repository_url": self.repo.url, "revision_hash": "new_revision_hash", "worker_slug": self.worker.slug, + "worker_name": self.worker.name, + "worker_type": self.worker.type.slug, "revision_message": "Bruce was very clever", "revision_author": "Iwan Roberts", "revision_references": [ @@ -421,7 +516,9 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "docker_image_iid": "some_docker_image", "repository_url": self.repo.url, "revision_hash": self.version.revision.hash, - "worker_slug": self.version.worker.slug, + "worker_slug": self.worker.slug, + "worker_name": self.worker.name, + "worker_type": self.worker.type.slug, "revision_references": [ {"type": "tag", "name": "9.9.9.9.9.9"}, ], @@ -474,11 +571,11 @@ class TestDockerWorkerVersion(FixtureAPITestCase): def test_create_git_stack(self): """ All git references can be created including repository, revision, gitrefs and worker - including the default worker type + and worker type """ self.worker_type.delete() self.client.force_login(self.user) - with self.assertNumQueries(29): + with self.assertNumQueries(30): response = self.client.post( reverse("api:version-from-docker"), data={ @@ -487,6 +584,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "repository_url": "https://gitlab.test.arkindex.org/project/", "revision_hash": "deadbeef", "worker_slug": "new_gen_classifier", + "worker_name": "Classifier New Generation", + "worker_type": "a_new_type", "revision_references": [ {"type": "branch", "name": "master"}, ], @@ -521,7 +620,7 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "created": new_version.created.isoformat().replace("+00:00", "Z"), "worker": { "id": str(new_version.worker.id), - "name": "new_gen_classifier", + "name": "Classifier New Generation", "slug": "new_gen_classifier", "type": str(new_worker_type), } @@ -533,7 +632,7 @@ class TestDockerWorkerVersion(FixtureAPITestCase): self.assertEqual(new_revision.repo, new_repo) self.assertEqual(new_ref.name, "master") self.assertEqual(new_ref.repository, new_repo) - self.assertEqual(new_version.worker.name, "new_gen_classifier") + self.assertEqual(new_version.worker.name, "Classifier New Generation") self.assertEqual(new_version.worker.slug, "new_gen_classifier") self.assertEqual(new_version.worker.public, False) self.assertEqual(new_version.worker.repository, new_repo) @@ -542,8 +641,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): self.assertEqual(new_version.docker_image_iid, "docker_image_42") self.assertEqual(new_version.gpu_usage, FeatureUsage.Disabled) self.assertEqual(new_version.model_usage, FeatureUsage.Disabled) - self.assertEqual(new_worker_type.slug, "docker") - self.assertEqual(new_worker_type.display_name, "Docker") + self.assertEqual(new_worker_type.slug, "a_new_type") + self.assertEqual(new_worker_type.display_name, "a_new_type") # User is granted an admin role on the repository self.assertListEqual(list(new_repo.memberships.values_list("user", "level")), [ (self.user.id, Role.Admin.value) @@ -564,6 +663,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "repository_url": self.repo.url, "revision_hash": "new_revision_hash", "worker_slug": self.worker.slug, + "worker_name": self.worker.name, + "worker_type": self.worker.type.slug, "configuration": { "user_configuration": { "demo_integer": {"title": "Demo Integer", "type": "int", "required": True, "default": 1}, @@ -707,6 +808,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "repository_url": self.repo.url, "revision_hash": "new_revision_hash", "worker_slug": self.worker.slug, + "worker_name": self.worker.name, + "worker_type": self.worker.type.slug, "configuration": { "user_configuration": user_configuration }, @@ -744,6 +847,8 @@ class TestDockerWorkerVersion(FixtureAPITestCase): "repository_url": self.repo.url, "revision_hash": "new_revision_hash", "worker_slug": self.worker.slug, + "worker_name": self.worker.name, + "worker_type": self.worker.type.slug, "configuration": { "user_configuration": { "param": {"title": "param", **params}