• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SwissDataScienceCenter / renku-data-services / 5388199646

27 Jun 2023 09:35AM UTC coverage: 88.917% (+0.4%) from 88.567%
5388199646

push

gihub-action

web-flow
chore: changes for Postgres and Helm (#9)

Co-authored-by: Johann-Michael Thiebaut <johann.thiebaut@gmail.com>
Co-authored-by: Alessandro Degano <40891147+aledegano@users.noreply.github.com>
Co-authored-by: Ralf Grubenmann <ralf.grubenmann@protonmail.com>

654 of 654 new or added lines in 15 files covered. (100.0%)

1428 of 1606 relevant lines covered (88.92%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.16
/src/renku_crc/app.py
1
"""Compute resource control (CRC) app."""
1✔
2
import asyncio
1✔
3
from dataclasses import asdict, dataclass
1✔
4
from typing import Any, Dict, List
1✔
5

6
from sanic import HTTPResponse, Request, Sanic, json
1✔
7
from sanic_ext import validate
1✔
8

9
import models
1✔
10
from db.adapter import ResourcePoolRepository, UserRepository
1✔
11
from k8s.quota import QuotaRepository
1✔
12
from models import errors
1✔
13
from renku_crc.auth import authenticate, only_admins
1✔
14
from renku_crc.blueprint import BlueprintFactoryResponse, CustomBlueprint
1✔
15
from renku_crc.config import Config
1✔
16
from renku_crc.error_handler import CustomErrorHandler
1✔
17
from schemas import apispec
1✔
18

19

20
@dataclass(kw_only=True)
1✔
21
class ResourcePoolsBP(CustomBlueprint):
1✔
22
    """Handlers for manipulating resource pools."""
1✔
23

24
    rp_repo: ResourcePoolRepository
1✔
25
    user_repo: UserRepository
1✔
26
    authenticator: models.Authenticator
1✔
27
    quota_repo: QuotaRepository
1✔
28

29
    def get_all(self) -> BlueprintFactoryResponse:
1✔
30
        """List all resource pools."""
31

32
        @authenticate(self.authenticator)
1✔
33
        async def _get_all(_: Request, user: models.APIUser):
1✔
34
            pool = asyncio.get_running_loop()
1✔
35
            rps: List[models.ResourcePool]
36
            quotas: List[models.Quota]
37
            rps, quotas = await asyncio.gather(
1✔
38
                self.rp_repo.get_resource_pools(api_user=user),
39
                pool.run_in_executor(None, self.quota_repo.get_quotas),
40
            )
41
            quotas_dict = {quota.id: quota for quota in quotas}
1✔
42
            rps_w_quota: List[models.ResourcePool] = []
1✔
43
            for rp in rps:
1✔
44
                quota = quotas_dict.get(rp.quota) if isinstance(rp.quota, str) else None
1✔
45
                if quota is not None:
1✔
46
                    rp_w_quota = rp.set_quota(quota)
×
47
                    rps_w_quota.append(rp_w_quota)
×
48
                else:
49
                    rps_w_quota.append(rp)
1✔
50

51
            return json([apispec.ResourcePoolWithId.from_orm(r).dict(exclude_none=True) for r in rps_w_quota])
1✔
52

53
        return "/resource_pools", ["GET"], _get_all
1✔
54

55
    def post(self) -> BlueprintFactoryResponse:
1✔
56
        """Add a new resource pool."""
57

58
        @authenticate(self.authenticator)
1✔
59
        @only_admins
1✔
60
        @validate(json=apispec.ResourcePool)
1✔
61
        async def _post(_: Request, body: apispec.ResourcePool, user: models.APIUser):
1✔
62
            rp = models.ResourcePool.from_dict(body.dict())
1✔
63
            if not isinstance(rp.quota, models.Quota):
1✔
64
                raise errors.ValidationError(message="The quota in the resource pool is malformed.")
×
65
            quota_with_id = rp.quota.generate_id()
1✔
66
            rp = rp.set_quota(quota_with_id)
1✔
67
            self.quota_repo.create_quota(quota_with_id)
1✔
68
            res = await self.rp_repo.insert_resource_pool(api_user=user, resource_pool=rp)
1✔
69
            res = res.set_quota(quota_with_id)
1✔
70
            return json(apispec.ResourcePoolWithId.from_orm(res).dict(exclude_none=True), 201)
1✔
71

72
        return "/resource_pools", ["POST"], _post
1✔
73

74
    def get_one(self) -> BlueprintFactoryResponse:
1✔
75
        """Get a specific resource pool."""
76

77
        @authenticate(self.authenticator)
1✔
78
        async def _get_one(request: Request, resource_pool_id: int, user: models.APIUser):
1✔
79
            pool = asyncio.get_running_loop()
1✔
80
            rps: List[models.ResourcePool]
81
            quotas: List[models.Quota]
82
            rps, quotas = await asyncio.gather(
1✔
83
                self.rp_repo.get_resource_pools(api_user=user, id=resource_pool_id, name=request.args.get("name")),
84
                pool.run_in_executor(None, self.quota_repo.get_quotas),
85
            )
86
            if len(rps) < 1:
1✔
87
                raise errors.MissingResourceError(
×
88
                    message=f"The resource pool with id {resource_pool_id} cannot be found."
89
                )
90
            rp = rps[0]
1✔
91
            quotas = [i for i in quotas if i.id == rp.quota]
1✔
92
            if len(quotas) >= 1:
1✔
93
                quota = quotas[0]
1✔
94
                rp = rp.set_quota(quota)
1✔
95
            return json(apispec.ResourcePoolWithId.from_orm(rp).dict(exclude_none=True))
1✔
96

97
        return "/resource_pools/<resource_pool_id>", ["GET"], _get_one
1✔
98

99
    def delete(self) -> BlueprintFactoryResponse:
1✔
100
        """Delete a specific resource pool."""
101

102
        @authenticate(self.authenticator)
1✔
103
        @only_admins
1✔
104
        async def _delete(_: Request, resource_pool_id: int, user: models.APIUser):
1✔
105
            rp = await self.rp_repo.delete_resource_pool(api_user=user, id=resource_pool_id)
1✔
106
            if rp is not None and isinstance(rp.quota, str):
1✔
107
                self.quota_repo.delete_quota(rp.quota)
×
108
            return HTTPResponse(status=204)
1✔
109

110
        return "/resource_pools/<resource_pool_id>", ["DELETE"], _delete
1✔
111

112
    def put(self) -> BlueprintFactoryResponse:
1✔
113
        """Update all fields of a specific resource pool."""
114

115
        @authenticate(self.authenticator)
1✔
116
        @only_admins
1✔
117
        @validate(json=apispec.ResourcePoolPut)
1✔
118
        async def _put(_: Request, resource_pool_id: int, body: apispec.ResourcePoolPut, user: models.APIUser):
1✔
119
            return await self._put_patch_resource_pool(api_user=user, resource_pool_id=resource_pool_id, body=body)
1✔
120

121
        return "/resource_pools/<resource_pool_id>", ["PUT"], _put
1✔
122

123
    def patch(self) -> BlueprintFactoryResponse:
1✔
124
        """Partially update a specific resource pool."""
125

126
        @authenticate(self.authenticator)
1✔
127
        @only_admins
1✔
128
        @validate(json=apispec.ResourcePoolPatch)
1✔
129
        async def _patch(_: Request, resource_pool_id: int, body: apispec.ResourcePoolPatch, user: models.APIUser):
1✔
130
            return await self._put_patch_resource_pool(api_user=user, resource_pool_id=resource_pool_id, body=body)
1✔
131

132
        return "/resource_pools/<resource_pool_id>", ["PATCH"], _patch
1✔
133

134
    async def _put_patch_resource_pool(
1✔
135
        self, api_user: models.APIUser, resource_pool_id: int, body: apispec.ResourcePoolPut | apispec.ResourcePoolPatch
136
    ):
137
        body_dict = body.dict(exclude_none=True)
1✔
138
        quota_req = body_dict.pop("quota", None)
1✔
139
        if quota_req is not None:
1✔
140
            rps = await self.rp_repo.get_resource_pools(api_user, resource_pool_id)
1✔
141
            if len(rps) == 0:
1✔
142
                raise errors.ValidationError(message=f"The resource pool with ID {resource_pool_id} does not exist.")
1✔
143
            rp = rps[0]
×
144
            if isinstance(rp.quota, str):
×
145
                quota_req["id"] = rp.quota
×
146
            quota_model = models.Quota.from_dict(quota_req)
×
147
            if quota_model.id is None:
×
148
                quota_model = quota_model.generate_id()
×
149
            self.quota_repo.update_quota(quota_model)
×
150
            if rp.quota is None:
×
151
                body_dict["quota"] = quota_model.id
×
152
        res = await self.rp_repo.update_resource_pool(
1✔
153
            api_user=api_user,
154
            id=resource_pool_id,
155
            **body_dict,
156
        )
157
        if res is None:
×
158
            raise errors.MissingResourceError(message=f"The resource pool with ID {resource_pool_id} cannot be found.")
×
159
        return json(apispec.ResourcePoolWithId.from_orm(res).dict(exclude_none=True))
×
160

161

162
@dataclass(kw_only=True)
1✔
163
class ResourcePoolUsersBP(CustomBlueprint):
1✔
164
    """Handlers for dealing with the users of individual resource pools."""
1✔
165

166
    repo: UserRepository
1✔
167
    authenticator: models.Authenticator
1✔
168

169
    def get_all(self) -> BlueprintFactoryResponse:
1✔
170
        """Get all users of a specific resource pool."""
171

172
        @authenticate(self.authenticator)
1✔
173
        @only_admins
1✔
174
        async def _get_all(_: Request, resource_pool_id: int, user: models.APIUser):
1✔
175
            res = await self.repo.get_users(api_user=user, resource_pool_id=resource_pool_id)
1✔
176
            return json([apispec.UserWithId(id=r.keycloak_id).dict(exclude_none=True) for r in res])
×
177

178
        return "/resource_pools/<resource_pool_id>/users", ["GET"], _get_all
1✔
179

180
    def post(self) -> BlueprintFactoryResponse:
1✔
181
        """Add users to a specific resource pool."""
182

183
        @authenticate(self.authenticator)
1✔
184
        @only_admins
1✔
185
        async def _post(request: Request, resource_pool_id: int, user: models.APIUser):
1✔
186
            users = apispec.UsersWithId.parse_obj(request.json)  # validation
1✔
187
            return await self._put_post(api_user=user, resource_pool_id=resource_pool_id, body=users, post=True)
1✔
188

189
        return "/resource_pools/<resource_pool_id>/users", ["POST"], _post
1✔
190

191
    def put(self) -> BlueprintFactoryResponse:
1✔
192
        """Set the users for a specific resource pool."""
193

194
        @authenticate(self.authenticator)
1✔
195
        @only_admins
1✔
196
        async def _put(request: Request, resource_pool_id: int, user: models.APIUser):
1✔
197
            users = apispec.UsersWithId.parse_obj(request.json)  # validation
1✔
198
            return await self._put_post(api_user=user, resource_pool_id=resource_pool_id, body=users, post=False)
1✔
199

200
        return "/resource_pools/<resource_pool_id>/users", ["PUT"], _put
1✔
201

202
    async def _put_post(
1✔
203
        self, api_user: models.APIUser, resource_pool_id: int, body: apispec.UsersWithId, post: bool = True
204
    ):
205
        users_to_add = [models.User(keycloak_id=user.id) for user in body.__root__]
1✔
206
        updated_users = await self.repo.update_resource_pool_users(
1✔
207
            api_user=api_user, resource_pool_id=resource_pool_id, users=users_to_add, append=post
208
        )
209
        return json(
×
210
            [apispec.UserWithId(id=r.keycloak_id).dict(exclude_none=True) for r in updated_users],
211
            status=201 if post else 200,
212
        )
213

214
    def get(self) -> BlueprintFactoryResponse:
1✔
215
        """Get a specific user of a specific resource pool."""
216

217
        @authenticate(self.authenticator)
1✔
218
        async def _get(_: Request, resource_pool_id: int, user_id: str, user: models.APIUser):
1✔
219
            res = await self.repo.get_users(keycloak_id=user_id, resource_pool_id=resource_pool_id, api_user=user)
1✔
220
            if len(res) < 1:
×
221
                raise errors.MissingResourceError(
×
222
                    message=f"The user with id {user_id} or resource pool with id {resource_pool_id} cannot be found."
223
                )
224
            return json(apispec.UserWithId(id=res[0].keycloak_id).dict(exclude_none=True))
×
225

226
        return "/resource_pools/<resource_pool_id>/users/<user_id>", ["GET"], _get
1✔
227

228
    def delete(self) -> BlueprintFactoryResponse:
1✔
229
        """Delete a specific user of a specific resource pool."""
230

231
        @authenticate(self.authenticator)
1✔
232
        @only_admins
1✔
233
        async def _delete(_: Request, resource_pool_id: int, user_id: str, user: models.APIUser):
1✔
234
            await self.repo.delete_resource_pool_user(
1✔
235
                resource_pool_id=resource_pool_id, keycloak_id=user_id, api_user=user
236
            )
237
            return HTTPResponse(status=204)
1✔
238

239
        return "/resource_pools/<resource_pool_id>/users/<user_id>", ["DELETE"], _delete
1✔
240

241

242
@dataclass(kw_only=True)
1✔
243
class ClassesBP(CustomBlueprint):
1✔
244
    """Handlers for dealing with resource classes of an individual resource pool."""
1✔
245

246
    repo: ResourcePoolRepository
1✔
247
    authenticator: models.Authenticator
1✔
248

249
    def get_all(self) -> BlueprintFactoryResponse:
1✔
250
        """Get the classes of a specific resource pool."""
251

252
        @authenticate(self.authenticator)
1✔
253
        async def _get_all(request: Request, resource_pool_id: int, user: models.APIUser):
1✔
254
            res = await self.repo.get_classes(
1✔
255
                api_user=user, resource_pool_id=resource_pool_id, name=request.args.get("name")
256
            )
257
            return json([apispec.ResourceClassWithId.from_orm(r).dict(exclude_none=True) for r in res])
1✔
258

259
        return "/resource_pools/<resource_pool_id>/classes", ["GET"], _get_all
1✔
260

261
    def post(self) -> BlueprintFactoryResponse:
1✔
262
        """Add a class to a specific resource pool."""
263

264
        @authenticate(self.authenticator)
1✔
265
        @only_admins
1✔
266
        @validate(json=apispec.ResourceClass)
1✔
267
        async def _post(_: Request, body: apispec.ResourceClass, resource_pool_id: int, user: models.APIUser):
1✔
268
            cls = models.ResourceClass.from_dict(body.dict())
1✔
269
            res = await self.repo.insert_resource_class(
1✔
270
                api_user=user, resource_class=cls, resource_pool_id=resource_pool_id
271
            )
272
            return json(apispec.ResourceClassWithId.from_orm(res).dict(exclude_none=True), 201)
×
273

274
        return "/resource_pools/<resource_pool_id>/classes", ["POST"], _post
1✔
275

276
    def get(self) -> BlueprintFactoryResponse:
1✔
277
        """Get a specific class of a specific resource pool."""
278

279
        @authenticate(self.authenticator)
1✔
280
        async def _get(_: Request, resource_pool_id: int, class_id: int, user: models.APIUser):
1✔
281
            res = await self.repo.get_classes(api_user=user, resource_pool_id=resource_pool_id, id=class_id)
1✔
282
            if len(res) < 1:
1✔
283
                raise errors.MissingResourceError(
1✔
284
                    message=f"The class with id {class_id} or resource pool with id {resource_pool_id} cannot be found."
285
                )
286
            return json(apispec.ResourceClassWithId.from_orm(res[0]).dict(exclude_none=True))
×
287

288
        return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["GET"], _get
1✔
289

290
    def get_no_pool(self) -> BlueprintFactoryResponse:
1✔
291
        """Get a specific class."""
292

293
        @authenticate(self.authenticator)
1✔
294
        async def _get_no_pool(_: Request, class_id: int, user: models.APIUser):
1✔
295
            res = await self.repo.get_classes(api_user=user, id=class_id)
1✔
296
            if len(res) < 1:
1✔
297
                raise errors.MissingResourceError(message=f"The class with id {class_id} cannot be found.")
1✔
298
            return json(apispec.ResourceClassWithId.from_orm(res[0]).dict(exclude_none=True))
×
299

300
        return "/classes/<class_id>", ["GET"], _get_no_pool
1✔
301

302
    def delete(self) -> BlueprintFactoryResponse:
1✔
303
        """Delete a specific class from a specific resource pool."""
304

305
        @authenticate(self.authenticator)
1✔
306
        @only_admins
1✔
307
        async def _delete(_: Request, resource_pool_id: int, class_id: int, user: models.APIUser):
1✔
308
            await self.repo.delete_resource_class(
1✔
309
                api_user=user, resource_pool_id=resource_pool_id, resource_class_id=class_id
310
            )
311
            return HTTPResponse(status=204)
1✔
312

313
        return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["DELETE"], _delete
1✔
314

315
    def put(self) -> BlueprintFactoryResponse:
1✔
316
        """Update all fields of a specific resource class for a specific resource pool."""
317

318
        @authenticate(self.authenticator)
1✔
319
        @only_admins
1✔
320
        @validate(json=apispec.ResourceClass)
1✔
321
        async def _put(
1✔
322
            _: Request, body: apispec.ResourceClass, resource_pool_id: int, class_id: int, user: models.APIUser
323
        ):
324
            return await self._put_patch(user, resource_pool_id, class_id, body)
1✔
325

326
        return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["PUT"], _put
1✔
327

328
    def patch(self) -> BlueprintFactoryResponse:
1✔
329
        """Partially update a specific resource class for a specific resource pool."""
330

331
        @authenticate(self.authenticator)
1✔
332
        @only_admins
1✔
333
        @validate(json=apispec.ResourceClassPatch)
1✔
334
        async def _patch(
1✔
335
            _: Request, body: apispec.ResourceClassPatch, resource_pool_id: int, class_id: int, user: models.APIUser
336
        ):
337
            return await self._put_patch(user, resource_pool_id, class_id, body)
1✔
338

339
        return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["PATCH"], _patch
1✔
340

341
    async def _put_patch(
1✔
342
        self,
343
        api_user: models.APIUser,
344
        resource_pool_id: int,
345
        class_id: int,
346
        body: apispec.ResourceClassPatch | apispec.ResourceClass,
347
    ):
348
        cls = await self.repo.update_resource_class(
1✔
349
            api_user=api_user,
350
            resource_pool_id=resource_pool_id,
351
            resource_class_id=class_id,
352
            **body.dict(exclude_none=True),
353
        )
354
        return json(apispec.ResourceClassWithId.from_orm(cls).dict(exclude_none=True))
1✔
355

356

357
@dataclass(kw_only=True)
1✔
358
class QuotaBP(CustomBlueprint):
1✔
359
    """Handlers for dealing with a quota."""
1✔
360

361
    rp_repo: ResourcePoolRepository
1✔
362
    quota_repo: QuotaRepository
1✔
363
    authenticator: models.Authenticator
1✔
364

365
    def get(self) -> BlueprintFactoryResponse:
1✔
366
        """Get the quota for a specific resource pool."""
367

368
        @authenticate(self.authenticator)
1✔
369
        async def _get(_: Request, resource_pool_id: int, user: models.APIUser):
1✔
370
            rps = await self.rp_repo.get_resource_pools(api_user=user, id=resource_pool_id)
1✔
371
            if len(rps) < 1:
1✔
372
                raise errors.MissingResourceError(
1✔
373
                    message=f"The resource pool with ID {resource_pool_id} cannot be found."
374
                )
375
            rp = rps[0]
1✔
376
            if rp.quota is None:
1✔
377
                raise errors.MissingResourceError(
×
378
                    message=f"The resource pool with ID {resource_pool_id} does not have a quota."
379
                )
380
            if not isinstance(rp.quota, str):
1✔
381
                raise errors.ValidationError(message="The quota in the resource pool should be a string.")
×
382
            quotas = self.quota_repo.get_quotas(name=rp.quota)
1✔
383
            if len(quotas) < 1:
1✔
384
                raise errors.MissingResourceError(
×
385
                    message=f"Cannot find the quota with name {rp.quota} "
386
                    f"for the resource pool with ID {resource_pool_id}."
387
                )
388
            quota = quotas[0]
1✔
389
            return json(apispec.QuotaWithId.from_orm(quota).dict(exclude_none=True))
1✔
390

391
        return "/resource_pools/<resource_pool_id>/quota", ["GET"], _get
1✔
392

393
    def put(self) -> BlueprintFactoryResponse:
1✔
394
        """Update all fields of a the quota of a specific resource pool."""
395

396
        @authenticate(self.authenticator)
1✔
397
        @only_admins
1✔
398
        @validate(json=apispec.QuotaWithId)
1✔
399
        async def _put(_: Request, resource_pool_id: int, body: apispec.QuotaWithId, user: models.APIUser):
1✔
400
            return await self._put_patch(resource_pool_id, body, api_user=user)
1✔
401

402
        return "/resource_pools/<resource_pool_id>/quota", ["PUT"], _put
1✔
403

404
    def patch(self) -> BlueprintFactoryResponse:
1✔
405
        """Partially update the quota of a specific resource pool."""
406

407
        @authenticate(self.authenticator)
1✔
408
        @only_admins
1✔
409
        @validate(json=apispec.QuotaPatch)
1✔
410
        async def _patch(_: Request, resource_pool_id: int, body: apispec.QuotaPatch, user: models.APIUser):
1✔
411
            return await self._put_patch(resource_pool_id, body, api_user=user)
1✔
412

413
        return "/resource_pools/<resource_pool_id>/quota", ["PATCH"], _patch
1✔
414

415
    async def _put_patch(
1✔
416
        self, resource_pool_id: int, body: apispec.QuotaPatch | apispec.QuotaWithId, api_user: models.APIUser
417
    ):
418
        rps = await self.rp_repo.get_resource_pools(api_user=api_user, id=resource_pool_id)
1✔
419
        if len(rps) < 1:
1✔
420
            raise errors.MissingResourceError(message=f"Cannot find the resource pool with ID {resource_pool_id}.")
1✔
421
        rp = rps[0]
1✔
422
        if rp.quota is None:
1✔
423
            raise errors.MissingResourceError(
×
424
                message=f"The resource pool with ID {resource_pool_id} does not have a quota."
425
            )
426
        if not isinstance(rp.quota, str):
1✔
427
            raise errors.ValidationError(message="The quota in the resource pool should be a string.")
×
428
        quotas = self.quota_repo.get_quotas(name=rp.quota)
1✔
429
        if len(quotas) < 1:
1✔
430
            raise errors.MissingResourceError(
×
431
                message=f"Cannot find the quota with name {rp.quota} for the resource pool with ID {resource_pool_id}."
432
            )
433
        old_quota = quotas[0]
1✔
434
        new_quota = models.Quota.from_dict({**asdict(old_quota), **body.dict(exclude_none=True)})
1✔
435
        self.quota_repo.update_quota(new_quota)
1✔
436
        return json(apispec.QuotaWithId.from_orm(new_quota).dict(exclude_none=True))
1✔
437

438

439
@dataclass(kw_only=True)
1✔
440
class UsersBP(CustomBlueprint):
1✔
441
    """Handlers for creating and listing users."""
1✔
442

443
    repo: UserRepository
1✔
444
    user_store: models.UserStore
1✔
445
    authenticator: models.Authenticator
1✔
446

447
    def post(self) -> BlueprintFactoryResponse:
1✔
448
        """Add a new user. The user has to exist in Keycloak."""
449

450
        @authenticate(self.authenticator)
1✔
451
        @only_admins
1✔
452
        @validate(json=apispec.UserWithId)
1✔
453
        async def _post(request: Request, body: apispec.UserWithId, user: models.APIUser):
1✔
454
            users_db, user_kc = await asyncio.gather(
1✔
455
                self.repo.get_users(keycloak_id=body.id, api_user=user),
456
                self.user_store.get_user_by_id(body.id, user.access_token),  # type: ignore[arg-type]
457
            )
458
            user_db = next(iter(users_db), None)
1✔
459
            # The user does not exist in keycloak, delete it form the crc database and fail.
460
            if user_kc is None:
1✔
461
                await self.repo.delete_user(id=body.id, api_user=user)
×
462
                raise errors.MissingResourceError(message=f"User with id {body.id} cannot be found in keycloak.")
×
463
            # The user exists in keycloak, fail if the requestd id does not match what is in keycloak.
464
            if body.id != user_kc.keycloak_id:
1✔
465
                raise errors.ValidationError(message="The provided user ID does not match the ID from keycloak.")
×
466
            # The user exists in the db and the request body matches what is the in the db, simply return the user.
467
            if user_db is not None and user_db.keycloak_id == body.id:
1✔
468
                return json(
×
469
                    apispec.UserWithId(id=user_db.keycloak_id).dict(exclude_none=True),
470
                    200,
471
                )
472
            # The user does not exist in the db, add it.
473
            kc_user = await self.repo.insert_user(api_user=user, user=models.User(keycloak_id=body.id))
1✔
474
            return json(
1✔
475
                apispec.UserWithId(id=kc_user.keycloak_id).dict(exclude_none=True),
476
                201,
477
            )
478

479
        return "/users", ["POST"], _post
1✔
480

481
    def get_all(self) -> BlueprintFactoryResponse:
1✔
482
        """Get all users. Please note that this is a subset of the users from Keycloak."""
483

484
        @authenticate(self.authenticator)
1✔
485
        @only_admins
1✔
486
        async def _get_all(_: Request, user: models.APIUser):
1✔
487
            res = await self.repo.get_users(api_user=user)
1✔
488
            return json([apispec.UserWithId(id=r.keycloak_id).dict(exclude_none=True) for r in res])
1✔
489

490
        return "/users", ["GET"], _get_all
1✔
491

492
    def delete(self) -> BlueprintFactoryResponse:
1✔
493
        """Delete a specific user, removing them from any resource pool they had access to."""
494

495
        @authenticate(self.authenticator)
1✔
496
        @only_admins
1✔
497
        async def _delete(_: Request, user_id: str, user: models.APIUser):
1✔
498
            await self.repo.delete_user(id=user_id, api_user=user)
1✔
499
            return HTTPResponse(status=204)
1✔
500

501
        return "/users/<user_id>", ["DELETE"], _delete
1✔
502

503

504
@dataclass(kw_only=True)
1✔
505
class UserResourcePoolsBP(CustomBlueprint):
1✔
506
    """Handlers for dealing wiht the resource pools of a specific user."""
1✔
507

508
    repo: UserRepository
1✔
509
    authenticator: models.Authenticator
1✔
510

511
    def get(self) -> BlueprintFactoryResponse:
1✔
512
        """Get all resource pools that a specific user has access to."""
513

514
        @authenticate(self.authenticator)
1✔
515
        @only_admins
1✔
516
        async def _get(_: Request, user_id: str, user: models.APIUser):
1✔
517
            rps = await self.repo.get_user_resource_pools(keycloak_id=user_id, api_user=user)
1✔
518
            return json([apispec.ResourcePoolWithId.from_orm(rp).dict(exclude_none=True) for rp in rps])
1✔
519

520
        return "/users/<user_id>/resource_pools", ["GET"], _get
1✔
521

522
    def post(self) -> BlueprintFactoryResponse:
1✔
523
        """Give a specific user access to a specific resource pool."""
524

525
        @authenticate(self.authenticator)
1✔
526
        @only_admins
1✔
527
        async def _post(request: Request, user_id: str, user: models.APIUser):
1✔
528
            ids = apispec.IntegerIds.parse_obj(request.json)  # validation
1✔
529
            return await self._post_put(user_id=user_id, post=True, resource_pool_ids=ids, api_user=user)
1✔
530

531
        return "/users/<user_id>/resource_pools", ["POST"], _post
1✔
532

533
    def put(self) -> BlueprintFactoryResponse:
1✔
534
        """Set the list of resource pools that a specific user has access to."""
535

536
        @authenticate(self.authenticator)
1✔
537
        @only_admins
1✔
538
        async def _put(request: Request, user_id: str, user: models.APIUser):
1✔
539
            ids = apispec.IntegerIds.parse_obj(request.json)  # validation
1✔
540
            return await self._post_put(user_id=user_id, post=False, resource_pool_ids=ids, api_user=user)
1✔
541

542
        return "/users/<user_id>/resource_pools", ["PUT"], _put
1✔
543

544
    async def _post_put(
1✔
545
        self, user_id: str, resource_pool_ids: apispec.IntegerIds, api_user: models.APIUser, post: bool = True
546
    ):
547
        rps = await self.repo.update_user_resource_pools(
1✔
548
            keycloak_id=user_id, resource_pool_ids=resource_pool_ids.__root__, append=post, api_user=api_user
549
        )
550
        return json([apispec.ResourcePoolWithId.from_orm(rp).dict(exclude_none=True) for rp in rps])
×
551

552

553
@dataclass(kw_only=True)
1✔
554
class MiscBP(CustomBlueprint):
1✔
555
    """Server contains all handlers for CRC and the configuration."""
1✔
556

557
    apispec: Dict[str, Any]
1✔
558
    version: str
1✔
559

560
    def get_apispec(self) -> BlueprintFactoryResponse:
1✔
561
        """Servers the OpenAPI specification."""
562

563
        async def _get_apispec(_: Request):
1✔
564
            return json(self.apispec)
1✔
565

566
        return "/spec.json", ["GET"], _get_apispec
1✔
567

568
    def get_error(self) -> BlueprintFactoryResponse:
1✔
569
        """Returns a sample error response."""
570

571
        async def _get_error(_: Request):
1✔
572
            raise errors.ValidationError(message="Sample validation error")
1✔
573

574
        return "/error", ["GET"], _get_error
1✔
575

576
    def get_version(self) -> BlueprintFactoryResponse:
1✔
577
        """Returns the version."""
578

579
        async def _get_version(_: Request):
1✔
580
            return json({"version": self.version})
1✔
581

582
        return "/version", ["GET"], _get_version
1✔
583

584

585
def register_all_handlers(app: Sanic, config: Config) -> Sanic:
1✔
586
    """Register all handlers on the application."""
587
    url_prefix = "/api/data"
1✔
588
    resource_pools = ResourcePoolsBP(
1✔
589
        name="resource_pools",
590
        url_prefix=url_prefix,
591
        rp_repo=config.rp_repo,
592
        authenticator=config.authenticator,
593
        user_repo=config.user_repo,
594
        quota_repo=config.quota_repo,
595
    )
596
    classes = ClassesBP(name="classes", url_prefix=url_prefix, repo=config.rp_repo, authenticator=config.authenticator)
1✔
597
    quota = QuotaBP(
1✔
598
        name="quota",
599
        url_prefix=url_prefix,
600
        rp_repo=config.rp_repo,
601
        authenticator=config.authenticator,
602
        quota_repo=config.quota_repo,
603
    )
604
    resource_pools_users = ResourcePoolUsersBP(
1✔
605
        name="resource_pool_users", url_prefix=url_prefix, repo=config.user_repo, authenticator=config.authenticator
606
    )
607
    users = UsersBP(
1✔
608
        name="users",
609
        url_prefix=url_prefix,
610
        repo=config.user_repo,
611
        user_store=config.user_store,
612
        authenticator=config.authenticator,
613
    )
614
    user_resource_pools = UserResourcePoolsBP(
1✔
615
        name="user_resource_pools", url_prefix=url_prefix, repo=config.user_repo, authenticator=config.authenticator
616
    )
617
    misc = MiscBP(name="misc", url_prefix=url_prefix, apispec=config.spec, version=config.version)
1✔
618
    app.blueprint(
1✔
619
        [
620
            resource_pools.blueprint(),
621
            classes.blueprint(),
622
            quota.blueprint(),
623
            resource_pools_users.blueprint(),
624
            users.blueprint(),
625
            user_resource_pools.blueprint(),
626
            misc.blueprint(),
627
        ]
628
    )
629

630
    app.error_handler = CustomErrorHandler()
1✔
631
    app.config.OAS = False
1✔
632
    app.config.OAS_UI_REDOC = False
1✔
633
    app.config.OAS_UI_SWAGGER = False
1✔
634
    app.config.OAS_AUTODOC = False
1✔
635
    return app
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc