diff --git a/components/renku_data_services/notebooks/apispec.py b/components/renku_data_services/notebooks/apispec.py index 860256726..eec402048 100644 --- a/components/renku_data_services/notebooks/apispec.py +++ b/components/renku_data_services/notebooks/apispec.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: api.spec.yaml -# timestamp: 2024-10-28T16:00:32+00:00 +# timestamp: 2024-12-11T13:03:50+00:00 from __future__ import annotations diff --git a/components/renku_data_services/session/api.spec.yaml b/components/renku_data_services/session/api.spec.yaml index 11b188344..d607cbe42 100644 --- a/components/renku_data_services/session/api.spec.yaml +++ b/components/renku_data_services/session/api.spec.yaml @@ -535,6 +535,9 @@ components: description: A container image type: string maxLength: 500 + # NOTE: regex for an image name, optionally with a tag or sha256 specified + # based on https://github.com/opencontainers/distribution-spec/blob/main/spec.md + pattern: "^[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*(\\/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}|@sha256:[a-fA-F0-9]{64}){0,1}$" example: renku/renkulab-py:3.10-0.18.1 DefaultUrl: description: The default path to open in a session diff --git a/components/renku_data_services/session/apispec.py b/components/renku_data_services/session/apispec.py index 1a14b30ab..94f84b00d 100644 --- a/components/renku_data_services/session/apispec.py +++ b/components/renku_data_services/session/apispec.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: api.spec.yaml -# timestamp: 2024-11-24T09:32:46+00:00 +# timestamp: 2024-12-11T13:31:04+00:00 from __future__ import annotations @@ -57,6 +57,7 @@ class Environment(BaseAPISpec): description="A container image", example="renku/renkulab-py:3.10-0.18.1", max_length=500, + pattern="^[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*(\\/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}|@sha256:[a-fA-F0-9]{64}){0,1}$", ) default_url: str = Field( ..., @@ -118,6 +119,7 @@ class EnvironmentPost(BaseAPISpec): description="A container image", example="renku/renkulab-py:3.10-0.18.1", max_length=500, + pattern="^[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*(\\/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}|@sha256:[a-fA-F0-9]{64}){0,1}$", ) default_url: str = Field( "/lab", @@ -178,6 +180,7 @@ class EnvironmentPatch(BaseAPISpec): description="A container image", example="renku/renkulab-py:3.10-0.18.1", max_length=500, + pattern="^[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*(\\/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}|@sha256:[a-fA-F0-9]{64}){0,1}$", ) default_url: Optional[str] = Field( None, diff --git a/test/bases/renku_data_services/data_api/test_sessions.py b/test/bases/renku_data_services/data_api/test_sessions.py index bd2dfc0e1..c0f1c71fa 100644 --- a/test/bases/renku_data_services/data_api/test_sessions.py +++ b/test/bases/renku_data_services/data_api/test_sessions.py @@ -80,11 +80,21 @@ async def test_get_session_environment( @pytest.mark.asyncio -async def test_post_session_environment(sanic_client: SanicASGITestClient, admin_headers) -> None: +@pytest.mark.parametrize( + "image_name", + [ + "renku/renku", + "u/renku/renku:latest", + "docker.io/renku/renku:latest", + "renku/renku@sha256:eceed25752d7544db159e4144a41ed6e96e667f39ff9fa18322d79c33729a18c", + "registry.renkulab.io/john.doe/test-34:38d8b3d", + ], +) +async def test_post_session_environment(sanic_client: SanicASGITestClient, admin_headers, image_name: str) -> None: payload = { "name": "Environment 1", "description": "A session environment.", - "container_image": "some_image:some_tag", + "container_image": image_name, } _, res = await sanic_client.post("/api/data/environments", headers=admin_headers, json=payload) @@ -93,7 +103,32 @@ async def test_post_session_environment(sanic_client: SanicASGITestClient, admin assert res.json is not None assert res.json.get("name") == "Environment 1" assert res.json.get("description") == "A session environment." - assert res.json.get("container_image") == "some_image:some_tag" + assert res.json.get("container_image") == image_name + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "image_name", + [ + "https://example.com/r/test:latest", + "renku/_bla", + "renku/test:töst", + "renku/test@sha254:abcd", + " renku/test:latest", + ], +) +async def test_post_session_environment_invalid_image( + sanic_client: SanicASGITestClient, admin_headers, image_name: str +) -> None: + payload = { + "name": "Environment 1", + "description": "A session environment.", + "container_image": image_name, + } + + _, res = await sanic_client.post("/api/data/environments", headers=admin_headers, json=payload) + + assert res.status_code == 422, res.text @pytest.mark.asyncio