Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/585: PoC limitting user resources #591

Merged
merged 2 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions applications/jupyterhub/deploy/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ hub:
c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": "frame-ancestors 'self' localhost *.osb.local *.opensourcebrain.org *.v2.opensourcebrain.org"}}
spawner: >-
c.Spawner.args = []
namedServerLimitPerUser: 3
singleuser:
storage:
type: dynamic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ export default (props: WorkspaceEditProps) => {
},
(e) => {
setLoading(false);
if (e.status === 405) {
throw new Error("Maximum number of workspaces exceeded.");
}
throw new Error("Error submitting the workspace");
// console.error('Error submitting the workspace', e);
}
Expand Down
2 changes: 2 additions & 0 deletions applications/workspaces/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ paths:
$ref: "#/components/schemas/Workspace"
400:
description: The Workspace already exists.
405:
description: Not allowed to create a new workspace
/workspace/{id}:
parameters:
- in: path
Expand Down
2 changes: 2 additions & 0 deletions applications/workspaces/server/workspaces/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class Config(object):
APP_NAME = "workspaces"
WSMGR_HOSTNAME = socket.gethostname()
WSMGR_IPADDRESS = socket.gethostbyname(WSMGR_HOSTNAME)
# set the max number of workspaces per user
MAX_NUMBER_WORKSPACES_PER_USER = 3

try:
CH_NAMESPACE = conf.get_configuration()["namespace"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from workspaces.repository.model_repository import WorkspaceImageRepository, WorkspaceRepository, db
from workspaces.repository.models import WorkspaceEntity, WorkspaceImage
from workspaces.helpers.etl_helpers import copy_origins
from workspaces.service.model_service import NotAuthorized, WorkspaceService
from workspaces.service.model_service import NotAuthorized, NotAllowed, WorkspaceService

def _save_image(id_=None, image=None, filename_base=None):
ext = mimetypes.guess_extension(image.mimetype)
Expand Down Expand Up @@ -100,3 +100,5 @@ def workspace_clone(id_, body=None):
return ws.to_dict()
except NotAuthorized:
return "Not authorized", 401
except NotAllowed:
return "Not allowed", 405
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ paths:
$ref: '#/components/schemas/Workspace'
"400":
description: The Workspace already exists.
"405":
description: Not allowed to create a new workspace
/workspace/{id}:
parameters:
- in: path
Expand Down
24 changes: 20 additions & 4 deletions applications/workspaces/server/workspaces/service/model_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class NotAuthorized(Exception):
pass


class NotAllowed(Exception):
pass


class UserService():
def get(self, user_id):

Expand Down Expand Up @@ -131,9 +135,6 @@ def is_authorized(self, object):
raise NotImplementedError(
f"Authorization not implemented for {self.__class__.__name__}")





class WorkspaceService(BaseModelService):
repository = WorkspaceRepository()
Expand All @@ -144,11 +145,24 @@ class WorkspaceService(BaseModelService):
@staticmethod
def get_pvc_name(workspace_id):
return f"workspace-{workspace_id}"

def check_max_num_workspaces_per_user(self, user_id=None):
if not user_id:
user_id = keycloak_user_id()
# check if max number of ws per user limit is reached
num_ws_current_user = self.repository.search(user_id=user_id).total
max_num_ws_current_user = Config.MAX_NUMBER_WORKSPACES_PER_USER
if num_ws_current_user >= max_num_ws_current_user:
raise NotAllowed(
f"Max number of {max_num_ws_current_user} workspaces " \
"limit exceeded"
)

@send_event(message_type="workspace", operation="create")
def post(self, body):
if 'user_id' not in body:
body['user_id'] = keycloak_user_id()
self.check_max_num_workspaces_per_user(body['user_id'])
for r in body.get("resources", []):
r.update({"origin": json.dumps(r.get("origin"))})
workspace = Workspace.from_dict(body) # Validate
Expand All @@ -168,6 +182,8 @@ def get_workspace_volume_size(self, ws: Workspace):

@send_event(message_type="workspace", operation="create")
def clone(self, workspace_id):
user_id = keycloak_user_id()
self.check_max_num_workspaces_per_user(user_id)
from workspaces.service.workflow import clone_workspaces_content
workspace = self.get(workspace_id)
if workspace is None:
Expand All @@ -177,7 +193,7 @@ def clone(self, workspace_id):
cloned = dict(
name=f"Clone of {workspace['name']}",
tags=workspace['tags'],
user_id=keycloak_user_id(),
user_id=user_id,

description=workspace['description'],
publicable=False,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from workspaces.service.model_service import (
NotAuthorized,
NotAllowed,
OsbrepositoryService,
VolumestorageService,
WorkspaceService,
Expand All @@ -15,6 +16,12 @@

class WorkspaceView(BaseModelView):
service = WorkspaceService()

def post(self, body):
try:
super().post(body)
except NotAllowed:
return "Not allowed", 405


class OsbrepositoryView(BaseModelView):
Expand Down