diff --git a/src/ai/backend/manager/models/container_registry.py b/src/ai/backend/manager/models/container_registry.py index d7a60c4d0d..806bd612e6 100644 --- a/src/ai/backend/manager/models/container_registry.py +++ b/src/ai/backend/manager/models/container_registry.py @@ -227,6 +227,8 @@ async def mutate( input_config: Dict[str, Any] = {} + # Question: Can we also change the hostname here (through props)? + # If it should be possible, we should add the hostname to the `ModifyContainerRegistryInput` set_if_set(props, input_config, "url") set_if_set(props, input_config, "type") set_if_set(props, input_config, "username") diff --git a/tests/manager/conftest.py b/tests/manager/conftest.py index 0bc23a7daa..c4d40422b8 100644 --- a/tests/manager/conftest.py +++ b/tests/manager/conftest.py @@ -464,7 +464,7 @@ async def clean_fixture(): await conn.execute((users.delete())) await conn.execute((scaling_groups.delete())) await conn.execute((domains.delete())) - await conn.execute((load_table(engine, "container_registries").delete())) + await conn.execute(((await load_table(engine, "container_registries")).delete())) finally: await engine.dispose() diff --git a/tests/manager/models/test_container_registries.py b/tests/manager/models/test_container_registries.py index 9ac345dfab..7ad53cc9bb 100644 --- a/tests/manager/models/test_container_registries.py +++ b/tests/manager/models/test_container_registries.py @@ -4,7 +4,7 @@ from ai.backend.manager.defs import PASSWORD_PLACEHOLDER from ai.backend.manager.models.gql import GraphQueryContext, Mutations, Queries -from ai.backend.manager.models.utils import ExtendedAsyncSAEngine +from ai.backend.manager.models.utils import connect_database CONTAINER_REGISTRY_FIELDS = """ container_registry { @@ -26,217 +26,289 @@ def client() -> Client: return Client(Schema(query=Queries, mutation=Mutations, auto_camelcase=False)) -@pytest.fixture(scope="module") -def context(database_engine: ExtendedAsyncSAEngine) -> GraphQueryContext: # noqa: F811 - return GraphQueryContext( - schema=None, # type: ignore - dataloader_manager=None, # type: ignore - local_config=None, # type: ignore - shared_config=None, # type: ignore - etcd=None, # type: ignore - user={"domain": "default", "role": "superadmin"}, - access_key="AKIAIOSFODNN7EXAMPLE", - db=database_engine, # type: ignore - redis_stat=None, # type: ignore - redis_image=None, # type: ignore - redis_live=None, # type: ignore - manager_status=None, # type: ignore - known_slot_types=None, # type: ignore - background_task_manager=None, # type: ignore - storage_manager=None, # type: ignore - registry=None, # type: ignore - idle_checker_host=None, # type: ignore - ) - - @pytest.mark.dependency() @pytest.mark.asyncio -async def test_create_container_registry(client: Client, context: GraphQueryContext): - query = """ - mutation CreateContainerRegistry($hostname: String!, $props: CreateContainerRegistryInput!) { - create_container_registry(hostname: $hostname, props: $props) { - $CONTAINER_REGISTRY_FIELDS +async def test_create_container_registry(client: Client, local_config, database): + async with connect_database(local_config) as db: + context = GraphQueryContext( + schema=None, # type: ignore + dataloader_manager=None, # type: ignore + local_config=None, # type: ignore + shared_config=None, # type: ignore + etcd=None, # type: ignore + user={"domain": "default", "role": "superadmin"}, + access_key="AKIAIOSFODNN7EXAMPLE", + db=db, # type: ignore + redis_stat=None, # type: ignore + redis_image=None, # type: ignore + redis_live=None, # type: ignore + manager_status=None, # type: ignore + known_slot_types=None, # type: ignore + background_task_manager=None, # type: ignore + storage_manager=None, # type: ignore + registry=None, # type: ignore + idle_checker_host=None, # type: ignore + ) + + query = """ + mutation CreateContainerRegistry($hostname: String!, $props: CreateContainerRegistryInput!) { + create_container_registry(hostname: $hostname, props: $props) { + $CONTAINER_REGISTRY_FIELDS + } } + """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) + + variables = { + "hostname": "cr.example.com", + "props": { + "url": "http://cr.example.com", + "type": "docker", + "project": ["default"], + "username": "username", + "password": "password", + "ssl_verify": False, + }, } - """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) - variables = { - "hostname": "cr.example.com", - "props": { + response = await client.execute_async(query, variables=variables, context_value=context) + + container_registry = response["data"]["create_container_registry"]["container_registry"] + assert container_registry["hostname"] == "cr.example.com" + assert container_registry["config"] == { "url": "http://cr.example.com", "type": "docker", "project": ["default"], "username": "username", - "password": "password", + "password": PASSWORD_PLACEHOLDER, "ssl_verify": False, - }, - } - - response = await client.execute_async(query, variables=variables, context_value=context) - - container_registry = response["data"]["create_container_registry"]["container_registry"] - assert container_registry["hostname"] == "cr.example.com" - assert container_registry["config"] == { - "url": "http://cr.example.com", - "type": "docker", - "project": ["default"], - "username": "username", - "password": PASSWORD_PLACEHOLDER, - "ssl_verify": False, - } - - await context.db.dispose() + } @pytest.mark.dependency(depends=["test_create_container_registry"]) @pytest.mark.asyncio -async def test_modify_container_registry(client: Client, context: GraphQueryContext): - query = """ - mutation ModifyContainerRegistry($hostname: String!, $props: ModifyContainerRegistryInput!) { - modify_container_registry(hostname: $hostname, props: $props) { - $CONTAINER_REGISTRY_FIELDS +async def test_modify_container_registry(client: Client, local_config, database): + async with connect_database(local_config) as db: + context = GraphQueryContext( + schema=None, # type: ignore + dataloader_manager=None, # type: ignore + local_config=None, # type: ignore + shared_config=None, # type: ignore + etcd=None, # type: ignore + user={"domain": "default", "role": "superadmin"}, + access_key="AKIAIOSFODNN7EXAMPLE", + db=db, # type: ignore + redis_stat=None, # type: ignore + redis_image=None, # type: ignore + redis_live=None, # type: ignore + manager_status=None, # type: ignore + known_slot_types=None, # type: ignore + background_task_manager=None, # type: ignore + storage_manager=None, # type: ignore + registry=None, # type: ignore + idle_checker_host=None, # type: ignore + ) + + query = """ + mutation ModifyContainerRegistry($hostname: String!, $props: ModifyContainerRegistryInput!) { + modify_container_registry(hostname: $hostname, props: $props) { + $CONTAINER_REGISTRY_FIELDS + } } - } - """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) - - variables = { - "hostname": "cr.example.com", - "props": { - "username": "username2", - }, - } + """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) - response = await client.execute_async(query, variables=variables, context_value=context) - container_registry = response["data"]["modify_container_registry"]["container_registry"] - assert container_registry["hostname"] == "cr.example.com" - assert container_registry["config"]["url"] == "http://cr.example.com" - assert container_registry["config"]["type"] == "docker" - assert container_registry["config"]["project"] == ["default"] - assert container_registry["config"]["username"] == "username2" - assert container_registry["config"]["ssl_verify"] is False - - variables = { - "hostname": "cr.example.com", - "props": { - "url": "http://cr2.example.com", - "type": "harbor2", - "project": ["default", "example"], - }, - } + variables = { + "hostname": "cr.example.com", + "props": { + "username": "username2", + }, + } - response = await client.execute_async(query, variables=variables, context_value=context) - container_registry = response["data"]["modify_container_registry"]["container_registry"] - assert container_registry["hostname"] == "cr.example.com" - assert container_registry["config"]["url"] == "http://cr2.example.com" - assert container_registry["config"]["type"] == "harbor2" - assert container_registry["config"]["project"] == ["default", "example"] - assert container_registry["config"]["username"] == "username2" - assert container_registry["config"]["ssl_verify"] is False + response = await client.execute_async(query, variables=variables, context_value=context) + container_registry = response["data"]["modify_container_registry"]["container_registry"] + assert container_registry["hostname"] == "cr.example.com" + assert container_registry["config"]["url"] == "http://cr.example.com" + assert container_registry["config"]["type"] == "docker" + assert container_registry["config"]["project"] == ["default"] + assert container_registry["config"]["username"] == "username2" + assert container_registry["config"]["ssl_verify"] is False + + variables = { + "hostname": "cr.example.com", + "props": { + "url": "http://cr2.example.com", + "type": "harbor2", + "project": ["default", "example"], + }, + } - await context.db.dispose() + response = await client.execute_async(query, variables=variables, context_value=context) + container_registry = response["data"]["modify_container_registry"]["container_registry"] + assert container_registry["hostname"] == "cr.example.com" + assert container_registry["config"]["url"] == "http://cr2.example.com" + assert container_registry["config"]["type"] == "harbor2" + assert container_registry["config"]["project"] == ["default", "example"] + assert container_registry["config"]["username"] == "username2" + assert container_registry["config"]["ssl_verify"] is False @pytest.mark.dependency(depends=["test_modify_container_registry"]) @pytest.mark.asyncio async def test_modify_container_registry_allows_empty_string( - client: Client, context: GraphQueryContext + client: Client, local_config, database ): - query = """ - mutation ModifyContainerRegistry($hostname: String!, $props: ModifyContainerRegistryInput!) { - modify_container_registry(hostname: $hostname, props: $props) { - $CONTAINER_REGISTRY_FIELDS + async with connect_database(local_config) as db: + context = GraphQueryContext( + schema=None, # type: ignore + dataloader_manager=None, # type: ignore + local_config=None, # type: ignore + shared_config=None, # type: ignore + etcd=None, # type: ignore + user={"domain": "default", "role": "superadmin"}, + access_key="AKIAIOSFODNN7EXAMPLE", + db=db, # type: ignore + redis_stat=None, # type: ignore + redis_image=None, # type: ignore + redis_live=None, # type: ignore + manager_status=None, # type: ignore + known_slot_types=None, # type: ignore + background_task_manager=None, # type: ignore + storage_manager=None, # type: ignore + registry=None, # type: ignore + idle_checker_host=None, # type: ignore + ) + + query = """ + mutation ModifyContainerRegistry($hostname: String!, $props: ModifyContainerRegistryInput!) { + modify_container_registry(hostname: $hostname, props: $props) { + $CONTAINER_REGISTRY_FIELDS + } } + """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) + + # Given an empty string to password + variables = { + "hostname": "cr.example.com", + "props": { + "password": "", + }, } - """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) - - # Given an empty string to password - variables = { - "hostname": "cr.example.com", - "props": { - "password": "", - }, - } - - # Then password is set to empty string - response = await client.execute_async(query, variables=variables, context_value=context) - container_registry = response["data"]["modify_container_registry"]["container_registry"] - assert container_registry["hostname"] == "cr.example.com" - assert container_registry["config"]["url"] == "http://cr2.example.com" - assert container_registry["config"]["type"] == "harbor2" - assert container_registry["config"]["project"] == ["default", "example"] - assert container_registry["config"]["username"] == "username2" - assert container_registry["config"]["password"] == PASSWORD_PLACEHOLDER - assert container_registry["config"]["ssl_verify"] is False + # Then password is set to empty string + response = await client.execute_async(query, variables=variables, context_value=context) - await context.db.dispose() + container_registry = response["data"]["modify_container_registry"]["container_registry"] + assert container_registry["hostname"] == "cr.example.com" + assert container_registry["config"]["url"] == "http://cr2.example.com" + assert container_registry["config"]["type"] == "harbor2" + assert container_registry["config"]["project"] == ["default", "example"] + assert container_registry["config"]["username"] == "username2" + assert container_registry["config"]["password"] == PASSWORD_PLACEHOLDER + assert container_registry["config"]["ssl_verify"] is False @pytest.mark.dependency(depends=["test_modify_container_registry_allows_empty_string"]) @pytest.mark.asyncio async def test_modify_container_registry_allows_null_for_unset( - client: Client, context: GraphQueryContext + client: Client, local_config, database ): - query = """ - mutation ModifyContainerRegistry($hostname: String!, $props: ModifyContainerRegistryInput!) { - modify_container_registry(hostname: $hostname, props: $props) { - $CONTAINER_REGISTRY_FIELDS + async with connect_database(local_config) as db: + context = GraphQueryContext( + schema=None, # type: ignore + dataloader_manager=None, # type: ignore + local_config=None, # type: ignore + shared_config=None, # type: ignore + etcd=None, # type: ignore + user={"domain": "default", "role": "superadmin"}, + access_key="AKIAIOSFODNN7EXAMPLE", + db=db, # type: ignore + redis_stat=None, # type: ignore + redis_image=None, # type: ignore + redis_live=None, # type: ignore + manager_status=None, # type: ignore + known_slot_types=None, # type: ignore + background_task_manager=None, # type: ignore + storage_manager=None, # type: ignore + registry=None, # type: ignore + idle_checker_host=None, # type: ignore + ) + + query = """ + mutation ModifyContainerRegistry($hostname: String!, $props: ModifyContainerRegistryInput!) { + modify_container_registry(hostname: $hostname, props: $props) { + $CONTAINER_REGISTRY_FIELDS + } } + """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) + + # Given a null to password + variables = { + "hostname": "cr.example.com", + "props": { + "password": None, + }, } - """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) - - # Given a null to password - variables = { - "hostname": "cr.example.com", - "props": { - "password": None, - }, - } - # Then password is unset - response = await client.execute_async(query, variables=variables, context_value=context) - container_registry = response["data"]["modify_container_registry"]["container_registry"] - assert container_registry["hostname"] == "cr.example.com" - assert container_registry["config"]["url"] == "http://cr2.example.com" - assert container_registry["config"]["type"] == "harbor2" - assert container_registry["config"]["project"] == ["default", "example"] - assert container_registry["config"]["username"] == "username2" - assert container_registry["config"]["password"] is None - assert container_registry["config"]["ssl_verify"] is False - - await context.db.dispose() + # Then password is unset + response = await client.execute_async(query, variables=variables, context_value=context) + container_registry = response["data"]["modify_container_registry"]["container_registry"] + assert container_registry["hostname"] == "cr.example.com" + assert container_registry["config"]["url"] == "http://cr2.example.com" + assert container_registry["config"]["type"] == "harbor2" + assert container_registry["config"]["project"] == ["default", "example"] + assert container_registry["config"]["username"] == "username2" + assert container_registry["config"]["password"] is None + assert container_registry["config"]["ssl_verify"] is False @pytest.mark.dependency(depends=["test_modify_container_registry_allows_null_for_unset"]) @pytest.mark.asyncio -async def test_delete_container_registry(client: Client, context: GraphQueryContext): - query = """ - mutation DeleteContainerRegistry($hostname: String!) { - delete_container_registry(hostname: $hostname) { - $CONTAINER_REGISTRY_FIELDS +async def test_delete_container_registry(client: Client, local_config, database): + async with connect_database(local_config) as db: + context = GraphQueryContext( + schema=None, # type: ignore + dataloader_manager=None, # type: ignore + local_config=None, # type: ignore + shared_config=None, # type: ignore + etcd=None, # type: ignore + user={"domain": "default", "role": "superadmin"}, + access_key="AKIAIOSFODNN7EXAMPLE", + db=db, # type: ignore + redis_stat=None, # type: ignore + redis_image=None, # type: ignore + redis_live=None, # type: ignore + manager_status=None, # type: ignore + known_slot_types=None, # type: ignore + background_task_manager=None, # type: ignore + storage_manager=None, # type: ignore + registry=None, # type: ignore + idle_checker_host=None, # type: ignore + ) + + query = """ + mutation DeleteContainerRegistry($hostname: String!) { + delete_container_registry(hostname: $hostname) { + $CONTAINER_REGISTRY_FIELDS + } } - } - """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) + """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) - variables = { - "hostname": "cr.example.com", - } + variables = { + "hostname": "cr.example.com", + } - response = await client.execute_async(query, variables=variables, context_value=context) + response = await client.execute_async(query, variables=variables, context_value=context) - container_registry = response["data"]["delete_container_registry"]["container_registry"] - assert container_registry["hostname"] == "cr.example.com" + container_registry = response["data"]["delete_container_registry"]["container_registry"] + assert container_registry["hostname"] == "cr.example.com" - query = """ - query ContainerRegistry($hostname: String!) { - container_registry(hostname: $hostname) { - $CONTAINER_REGISTRY_FIELDS + query = """ + query ContainerRegistry($hostname: String!) { + container_registry(hostname: $hostname) { + $CONTAINER_REGISTRY_FIELDS + } } - } - """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) - - response = await client.execute_async(query, variables=variables, context_value=context) + """.replace("$CONTAINER_REGISTRY_FIELDS", CONTAINER_REGISTRY_FIELDS) - assert response["data"] is None + response = await client.execute_async(query, variables=variables, context_value=context) - # await context.db.dispose() + assert response["data"] is None