diff --git a/backend/capellacollab/alembic/versions/4d42177579a2_add_resource_override.py b/backend/capellacollab/alembic/versions/4d42177579a2_add_resource_override.py new file mode 100644 index 000000000..65382a1ab --- /dev/null +++ b/backend/capellacollab/alembic/versions/4d42177579a2_add_resource_override.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +"""Add resource override + +Revision ID: 4d42177579a2 +Revises: 320c5b39c509 +Create Date: 2024-11-12 17:43:23.486104 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "4d42177579a2" +down_revision = "320c5b39c509" +branch_labels = None +depends_on = None + + +t_tools = sa.Table( + "tools", + sa.MetaData(), + sa.Column("id", sa.Integer()), + sa.Column("integrations", postgresql.JSONB(astext_type=sa.Text())), + sa.Column("config", postgresql.JSONB(astext_type=sa.Text())), +) + + +def upgrade(): + connection = op.get_bind() + results = connection.execute(sa.select(t_tools)).mappings().all() + + for row in results: + config = row["config"] + config["resources"] = { + "default_profile": { + "cpu": config["resources"]["cpu"], + "memory": config["resources"]["memory"], + }, + "additional": {}, + } + + connection.execute( + sa.update(t_tools) + .where(t_tools.c.id == row["id"]) + .values(config=config) + ) diff --git a/backend/capellacollab/core/database/migration.py b/backend/capellacollab/core/database/migration.py index c39393bae..273232a2a 100644 --- a/backend/capellacollab/core/database/migration.py +++ b/backend/capellacollab/core/database/migration.py @@ -128,10 +128,13 @@ def get_eclipse_session_configuration() -> ( """ return tools_models.ToolSessionConfiguration( resources=tools_models.Resources( - cpu=tools_models.CPUResources(requests=0.4, limits=2), - memory=tools_models.MemoryResources( - requests="1.6Gi", limits="6Gi" + default_profile=tools_models.DefaultResourceProfile( + cpu=tools_models.CPUResources(requests=0.4, limits=2), + memory=tools_models.MemoryResources( + requests="1.6Gi", limits="6Gi" + ), ), + additional={}, ), environment={ "RMT_PASSWORD": "{CAPELLACOLLAB_SESSION_TOKEN}", @@ -273,10 +276,13 @@ def create_jupyter_tool(db: orm.Session) -> tools_models.DatabaseTool: integrations=tools_models.ToolIntegrations(jupyter=True), config=tools_models.ToolSessionConfiguration( resources=tools_models.Resources( - cpu=tools_models.CPUResources(requests=1, limits=2), - memory=tools_models.MemoryResources( - requests="500Mi", limits="3Gi" + default_profile=tools_models.DefaultResourceProfile( + cpu=tools_models.CPUResources(requests=1, limits=2), + memory=tools_models.MemoryResources( + requests="500Mi", limits="3Gi" + ), ), + additional={}, ), environment={ "JUPYTER_PORT": "8888", diff --git a/backend/capellacollab/projects/toolmodels/backups/routes.py b/backend/capellacollab/projects/toolmodels/backups/routes.py index 8e6927bac..5e56bae63 100644 --- a/backend/capellacollab/projects/toolmodels/backups/routes.py +++ b/backend/capellacollab/projects/toolmodels/backups/routes.py @@ -118,7 +118,7 @@ def create_backup( body.include_commit_history, ), labels=core.get_pipeline_labels(toolmodel), - tool_resources=toolmodel.tool.config.resources, + tool_resources=toolmodel.tool.config.resources.get_profile(None), command="backup", schedule=pipeline_config.cron, timezone=pipeline_config.timezone, diff --git a/backend/capellacollab/projects/toolmodels/backups/runs/interface.py b/backend/capellacollab/projects/toolmodels/backups/runs/interface.py index 29cb3d355..3517d4fe2 100644 --- a/backend/capellacollab/projects/toolmodels/backups/runs/interface.py +++ b/backend/capellacollab/projects/toolmodels/backups/runs/interface.py @@ -80,7 +80,9 @@ def _schedule_pending_jobs(): pending_run.pipeline.t4c_password, pending_run.pipeline.include_commit_history, ), - tool_resources=pending_run.pipeline.model.tool.config.resources, + tool_resources=pending_run.pipeline.model.tool.config.resources.get_profile( + None + ), ) pending_run.reference_id = job_name pending_run.status = models.PipelineRunStatus.SCHEDULED diff --git a/backend/capellacollab/sessions/operators/k8s.py b/backend/capellacollab/sessions/operators/k8s.py index a10a39034..ffb73f9dc 100644 --- a/backend/capellacollab/sessions/operators/k8s.py +++ b/backend/capellacollab/sessions/operators/k8s.py @@ -112,7 +112,7 @@ def start_session( ports=ports, volumes=volumes, init_volumes=init_volumes, - tool_resources=tool.config.resources, + tool_resources_profile=tool.config.resources.get_profile(username), annotations=annotations, labels=labels, ) @@ -267,7 +267,10 @@ def create_cronjob( image: str, command: str, labels: dict[str, str], - tool_resources: tools_models.Resources, + tool_resources: ( + tools_models.DefaultResourceProfile + | tools_models.AdditionalResourceProfile + ), environment: dict[str, str | None], schedule="* * * * *", timezone="UTC", @@ -309,7 +312,10 @@ def create_job( command: str, labels: dict[str, str], environment: dict[str, str | None], - tool_resources: tools_models.Resources, + tool_resources: ( + tools_models.DefaultResourceProfile + | tools_models.AdditionalResourceProfile + ), timeout: int = 18000, ) -> str: _id = self._generate_id() @@ -552,7 +558,10 @@ def _create_session_pod( ports: dict[str, int], volumes: list[models.Volume], init_volumes: list[models.Volume], - tool_resources: tools_models.Resources, + tool_resources_profile: ( + tools_models.DefaultResourceProfile + | tools_models.AdditionalResourceProfile + ), annotations: dict[str, str], labels: dict[str, str], ) -> client.V1Pod: @@ -566,12 +575,12 @@ def _create_session_pod( resources = client.V1ResourceRequirements( limits={ - "cpu": tool_resources.cpu.limits, - "memory": tool_resources.memory.limits, + "cpu": tool_resources_profile.cpu.limits, + "memory": tool_resources_profile.memory.limits, }, requests={ - "cpu": tool_resources.cpu.requests, - "memory": tool_resources.memory.requests, + "cpu": tool_resources_profile.cpu.requests, + "memory": tool_resources_profile.memory.requests, }, ) @@ -778,7 +787,10 @@ def _create_job_spec( image: str, job_labels: dict[str, str], environment: dict[str, str | None], - tool_resources: tools_models.Resources, + tool_resources: ( + tools_models.DefaultResourceProfile + | tools_models.AdditionalResourceProfile + ), args: list[str] | None = None, timeout: int = 18000, ) -> client.V1JobSpec: diff --git a/backend/capellacollab/tools/crud.py b/backend/capellacollab/tools/crud.py index 2c026514b..7851afebb 100644 --- a/backend/capellacollab/tools/crud.py +++ b/backend/capellacollab/tools/crud.py @@ -67,6 +67,24 @@ def update_tool( return tool +def update_tools_username( + db: orm.Session, old_username: str, new_username: str +): + tools = get_tools(db) + for tool in tools: + updated = False + for profile in tool.config.resources.additional.values(): + if old_username in profile.usernames: + profile.usernames = [ + new_username if username == old_username else username + for username in profile.usernames + ] + updated = True + if updated: + orm.attributes.flag_modified(tool, "config") + db.commit() + + def delete_tool(db: orm.Session, tool: models.DatabaseTool) -> None: db.delete(tool) db.commit() diff --git a/backend/capellacollab/tools/models.py b/backend/capellacollab/tools/models.py index 5115dca55..766499749 100644 --- a/backend/capellacollab/tools/models.py +++ b/backend/capellacollab/tools/models.py @@ -232,7 +232,7 @@ class MemoryResources(core_pydantic.BaseModel): ) -class Resources(core_pydantic.BaseModelStrict): +class DefaultResourceProfile(core_pydantic.BaseModel): cpu: CPUResources = pydantic.Field( default=CPUResources(), description="Configuration about the number of CPU cores that sessions can use.", @@ -243,6 +243,60 @@ class Resources(core_pydantic.BaseModelStrict): ) +class AdditionalResourceProfile(DefaultResourceProfile): + usernames: list[str] = pydantic.Field( + default=None, + description="List of usernames, which are allowed to use this resource profile.", + ) + + +class Resources(core_pydantic.BaseModelStrict): + default_profile: DefaultResourceProfile = pydantic.Field( + default_factory=DefaultResourceProfile, + description="Default resource profile, which is used when no other profile matches.", + ) + additional: dict[str, AdditionalResourceProfile] = pydantic.Field( + default={}, + description="Additional resource profiles, which can be used to limit the resource usage of sessions.", + ) + + def get_profile( + self, username: str | None + ) -> DefaultResourceProfile | AdditionalResourceProfile: + if username is None: + return self.default_profile + + for profile in self.additional.values(): + if username in profile.usernames: + return profile + + return self.default_profile + + @pydantic.field_validator("additional") + @classmethod + def check_additional_profiles( + cls, + value: dict[str, AdditionalResourceProfile], + ) -> dict[str, AdditionalResourceProfile]: + + for profile_name, profile in value.items(): + if len(profile.usernames) != len(set(profile.usernames)): + raise ValueError( + f"Usernames in profile '{profile_name}' must be unique." + ) + usernames = [set(profile.usernames) for profile in value.values()] + + all_usernames = [ + username for usernames in usernames for username in usernames + ] + + # Check that usernames aren't in multiple profiles + if len(all_usernames) != len(set(all_usernames)): + raise ValueError("Usernames must be unique across all profiles.") + + return value + + class PrometheusConfiguration(core_pydantic.BaseModel): path: str = pydantic.Field(default="/prometheus") diff --git a/backend/capellacollab/users/crud.py b/backend/capellacollab/users/crud.py index 6d855b750..7c1267adb 100644 --- a/backend/capellacollab/users/crud.py +++ b/backend/capellacollab/users/crud.py @@ -8,6 +8,7 @@ from sqlalchemy import orm from capellacollab.core import database +from capellacollab.tools import crud as tools_crud from capellacollab.users import models @@ -80,6 +81,8 @@ def create_user( def update_user( db: orm.Session, user: models.DatabaseUser, patch_user: models.PatchUser ) -> models.DatabaseUser: + if patch_user.name: + tools_crud.update_tools_username(db, user.name, patch_user.name) database.patch_database_with_pydantic_object(user, patch_user) db.commit() return user diff --git a/backend/tests/sessions/k8s_operator/test_session_k8s_operator.py b/backend/tests/sessions/k8s_operator/test_session_k8s_operator.py index 993b7e926..93e214055 100644 --- a/backend/tests/sessions/k8s_operator/test_session_k8s_operator.py +++ b/backend/tests/sessions/k8s_operator/test_session_k8s_operator.py @@ -118,7 +118,7 @@ def test_create_job(monkeypatch: pytest.MonkeyPatch): command="fakecmd", labels={"key": "value"}, environment={"ENVVAR": "value"}, - tool_resources=tools_models.Resources(), + tool_resources=tools_models.DefaultResourceProfile(), ) assert result @@ -136,7 +136,7 @@ def test_create_cronjob(monkeypatch: pytest.MonkeyPatch): command="fakecmd", environment={"ENVVAR": "value"}, labels={}, - tool_resources=tools_models.Resources(), + tool_resources=tools_models.DefaultResourceProfile(), ) assert result diff --git a/backend/tests/tools/test_tools_resources.py b/backend/tests/tools/test_tools_resources.py new file mode 100644 index 000000000..cfc5e69b1 --- /dev/null +++ b/backend/tests/tools/test_tools_resources.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import pydantic +import pytest + +from capellacollab.tools import models as tools_models + + +def test_validate_tools(): + with pytest.raises(pydantic.ValidationError): + tools_models.Resources( + default_profile=tools_models.DefaultResourceProfile(), + additional={ + "test1": tools_models.AdditionalResourceProfile( + usernames=["test", "test"] + ), + }, + ) + + with pytest.raises(pydantic.ValidationError): + tools_models.Resources( + default_profile=tools_models.DefaultResourceProfile(), + additional={ + "test1": tools_models.AdditionalResourceProfile( + usernames=["test"] + ), + "test2": tools_models.AdditionalResourceProfile( + usernames=["test"] + ), + }, + ) + + +def test_get_profile(): + default_profile = tools_models.DefaultResourceProfile( + memory=tools_models.MemoryResources(requests="1Gi", limits="2Gi"), + cpu=tools_models.CPUResources(requests=0.4, limits=2), + ) + different_profile = tools_models.AdditionalResourceProfile( + usernames=["testuser"], + memory=tools_models.MemoryResources(requests="1Gi", limits="2Gi"), + cpu=tools_models.CPUResources(requests=0.4, limits=2), + ) + + resources = tools_models.Resources( + default_profile=default_profile, + additional={ + "test": different_profile, + }, + ) + + resource_profile = resources.get_profile(None) + assert resource_profile == default_profile + + resource_profile = resources.get_profile("fakeuser") + assert resource_profile == default_profile + + resource_profile = resources.get_profile("testuser") + assert resource_profile == different_profile diff --git a/backend/tests/tools/test_tools_routes.py b/backend/tests/tools/test_tools_routes.py index 72ebd824a..c8a0544e3 100644 --- a/backend/tests/tools/test_tools_routes.py +++ b/backend/tests/tools/test_tools_routes.py @@ -18,8 +18,17 @@ def fixture_tools_json() -> dict: }, "config": { "resources": { - "cpu": {"limits": 2, "requests": 0.4}, - "memory": {"limits": "6Gi", "requests": "1.6Gi"}, + "default_profile": { + "cpu": {"limits": 2, "requests": 0.4}, + "memory": {"limits": "6Gi", "requests": "1.6Gi"}, + }, + "additional": { + "powerful": { + "cpu": {"limits": 2, "requests": 0.4}, + "memory": {"limits": "6Gi", "requests": "1.6Gi"}, + "usernames": ["testuser"], + }, + }, }, "connection": { "methods": [ diff --git a/docs/docs/admin/tools/configuration.md b/docs/docs/admin/tools/configuration.md index e60f844c3..4a6e6a859 100644 --- a/docs/docs/admin/tools/configuration.md +++ b/docs/docs/admin/tools/configuration.md @@ -33,18 +33,31 @@ For a full documentation of all available options, refer to the ### Resources For each tool, you can define the resources which sessions of the tool can use. -This is a significant option because it impacts cost and performance. +This is a significant option because it impacts cost and performance. If +certain users need more resources, you can define different resource profiles +for different users. An example configuration looks like this: ```yaml resources: - cpu: - requests: 0.4 - limits: 2 - memory: - requests: 1.6Gi - limits: 6Gi + profiles: + default: + cpu: + requests: 0.4 + limits: 2 + memory: + requests: 1.6Gi + limits: 6Gi + extra: + cpu: + requests: 0.8 + limits: 4 + memory: + requests: 1.9Gi + limits: 8Gi + users: + - testuser ``` The values are Kubernetes resource requests and limits. More information is diff --git a/frontend/src/app/openapi/.openapi-generator/FILES b/frontend/src/app/openapi/.openapi-generator/FILES index e09b20586..f70deeb2e 100644 --- a/frontend/src/app/openapi/.openapi-generator/FILES +++ b/frontend/src/app/openapi/.openapi-generator/FILES @@ -30,6 +30,8 @@ api/users.service.ts configuration.ts encoder.ts index.ts +model/additional-resource-profile-input.ts +model/additional-resource-profile-output.ts model/anonymized-session.ts model/authorization-response.ts model/backup-pipeline-run.ts @@ -52,6 +54,8 @@ model/create-tool-output.ts model/create-tool-version-input.ts model/create-tool-version-output.ts model/custom-navbar-link.ts +model/default-resource-profile-input.ts +model/default-resource-profile-output.ts model/diagram-cache-metadata.ts model/diagram-metadata.ts model/environment-value.ts diff --git a/frontend/src/app/openapi/model/additional-resource-profile-input.ts b/frontend/src/app/openapi/model/additional-resource-profile-input.ts new file mode 100644 index 000000000..f845fdbbd --- /dev/null +++ b/frontend/src/app/openapi/model/additional-resource-profile-input.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { MemoryResourcesInput } from './memory-resources-input'; +import { CPUResourcesInput } from './cpu-resources-input'; + + +export interface AdditionalResourceProfileInput { + /** + * Configuration about the number of CPU cores that sessions can use. + */ + cpu?: CPUResourcesInput; + /** + * Configuration about the amount of memory that sessions can use. + */ + memory?: MemoryResourcesInput; + /** + * List of usernames, which are allowed to use this resource profile. + */ + usernames?: Array; +} + diff --git a/frontend/src/app/openapi/model/additional-resource-profile-output.ts b/frontend/src/app/openapi/model/additional-resource-profile-output.ts new file mode 100644 index 000000000..e92716ad4 --- /dev/null +++ b/frontend/src/app/openapi/model/additional-resource-profile-output.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { MemoryResourcesOutput } from './memory-resources-output'; +import { CPUResourcesOutput } from './cpu-resources-output'; + + +export interface AdditionalResourceProfileOutput { + /** + * Configuration about the number of CPU cores that sessions can use. + */ + cpu: CPUResourcesOutput; + /** + * Configuration about the amount of memory that sessions can use. + */ + memory: MemoryResourcesOutput; + /** + * List of usernames, which are allowed to use this resource profile. + */ + usernames: Array; +} + diff --git a/frontend/src/app/openapi/model/default-resource-profile-input.ts b/frontend/src/app/openapi/model/default-resource-profile-input.ts new file mode 100644 index 000000000..293c9ca54 --- /dev/null +++ b/frontend/src/app/openapi/model/default-resource-profile-input.ts @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { MemoryResourcesInput } from './memory-resources-input'; +import { CPUResourcesInput } from './cpu-resources-input'; + + +export interface DefaultResourceProfileInput { + /** + * Configuration about the number of CPU cores that sessions can use. + */ + cpu?: CPUResourcesInput; + /** + * Configuration about the amount of memory that sessions can use. + */ + memory?: MemoryResourcesInput; +} + diff --git a/frontend/src/app/openapi/model/default-resource-profile-output.ts b/frontend/src/app/openapi/model/default-resource-profile-output.ts new file mode 100644 index 000000000..0be4756e8 --- /dev/null +++ b/frontend/src/app/openapi/model/default-resource-profile-output.ts @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { MemoryResourcesOutput } from './memory-resources-output'; +import { CPUResourcesOutput } from './cpu-resources-output'; + + +export interface DefaultResourceProfileOutput { + /** + * Configuration about the number of CPU cores that sessions can use. + */ + cpu: CPUResourcesOutput; + /** + * Configuration about the amount of memory that sessions can use. + */ + memory: MemoryResourcesOutput; +} + diff --git a/frontend/src/app/openapi/model/models.ts b/frontend/src/app/openapi/model/models.ts index 45ab795ce..dcfe86b34 100644 --- a/frontend/src/app/openapi/model/models.ts +++ b/frontend/src/app/openapi/model/models.ts @@ -9,6 +9,8 @@ + To generate a new version, run `make openapi` in the root directory of this repository. */ +export * from './additional-resource-profile-input'; +export * from './additional-resource-profile-output'; export * from './anonymized-session'; export * from './authorization-response'; export * from './backup'; @@ -31,6 +33,8 @@ export * from './create-tool-output'; export * from './create-tool-version-input'; export * from './create-tool-version-output'; export * from './custom-navbar-link'; +export * from './default-resource-profile-input'; +export * from './default-resource-profile-output'; export * from './diagram-cache-metadata'; export * from './diagram-metadata'; export * from './environment-value'; diff --git a/frontend/src/app/openapi/model/resources-input.ts b/frontend/src/app/openapi/model/resources-input.ts index 03d29730e..5710473c5 100644 --- a/frontend/src/app/openapi/model/resources-input.ts +++ b/frontend/src/app/openapi/model/resources-input.ts @@ -9,18 +9,18 @@ + To generate a new version, run `make openapi` in the root directory of this repository. */ -import { MemoryResourcesInput } from './memory-resources-input'; -import { CPUResourcesInput } from './cpu-resources-input'; +import { DefaultResourceProfileInput } from './default-resource-profile-input'; +import { AdditionalResourceProfileInput } from './additional-resource-profile-input'; export interface ResourcesInput { /** - * Configuration about the number of CPU cores that sessions can use. + * Default resource profile, which is used when no other profile matches. */ - cpu?: CPUResourcesInput; + default_profile?: DefaultResourceProfileInput; /** - * Configuration about the amount of memory that sessions can use. + * Additional resource profiles, which can be used to limit the resource usage of sessions. */ - memory?: MemoryResourcesInput; + additional?: { [key: string]: AdditionalResourceProfileInput; }; } diff --git a/frontend/src/app/openapi/model/resources-output.ts b/frontend/src/app/openapi/model/resources-output.ts index 1a0345971..8e36acc80 100644 --- a/frontend/src/app/openapi/model/resources-output.ts +++ b/frontend/src/app/openapi/model/resources-output.ts @@ -9,18 +9,18 @@ + To generate a new version, run `make openapi` in the root directory of this repository. */ -import { MemoryResourcesOutput } from './memory-resources-output'; -import { CPUResourcesOutput } from './cpu-resources-output'; +import { DefaultResourceProfileOutput } from './default-resource-profile-output'; +import { AdditionalResourceProfileOutput } from './additional-resource-profile-output'; export interface ResourcesOutput { /** - * Configuration about the number of CPU cores that sessions can use. + * Default resource profile, which is used when no other profile matches. */ - cpu: CPUResourcesOutput; + default_profile: DefaultResourceProfileOutput; /** - * Configuration about the amount of memory that sessions can use. + * Additional resource profiles, which can be used to limit the resource usage of sessions. */ - memory: MemoryResourcesOutput; + additional: { [key: string]: AdditionalResourceProfileOutput; }; } diff --git a/frontend/src/storybook/tool.ts b/frontend/src/storybook/tool.ts index 839bcc7f7..eab02de4f 100644 --- a/frontend/src/storybook/tool.ts +++ b/frontend/src/storybook/tool.ts @@ -69,13 +69,28 @@ const defaultToolConfig: ToolSessionConfigurationOutput = { mounting_enabled: true, }, resources: { - cpu: { - requests: 0.5, - limits: 1, + default_profile: { + cpu: { + requests: 0.5, + limits: 1, + }, + memory: { + requests: '1Gi', + limits: '2Gi', + }, }, - memory: { - requests: '1Gi', - limits: '2Gi', + additional: { + max: { + cpu: { + requests: 0.5, + limits: 1, + }, + memory: { + requests: '1Gi', + limits: '2Gi', + }, + usernames: ['testuser'], + }, }, }, environment: {},