Skip to content

Commit 28dac67

Browse files
feat(rest): add quota management endpoints (#748)
Closes #747
1 parent e04d1fd commit 28dac67

File tree

10 files changed

+646
-76
lines changed

10 files changed

+646
-76
lines changed

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
"reana_server.rest.status",
231231
"reana_server.rest.users",
232232
"reana_server.rest.workflows",
233+
"reana_server.rest.quota",
233234
]
234235

235236

docs/openapi.json

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,209 @@
10321032
"summary": "Ping the server (healthcheck)"
10331033
}
10341034
},
1035+
"/api/quota": {
1036+
"get": {
1037+
"description": "This endpoint gets resource quota limits for a given user.",
1038+
"operationId": "list_quota_usage",
1039+
"parameters": [
1040+
{
1041+
"description": "REANA access token.",
1042+
"in": "query",
1043+
"name": "access_token",
1044+
"required": true,
1045+
"type": "string"
1046+
},
1047+
{
1048+
"description": "Get the quota limit by user ID (mutually exclusive with `email` and `user_access_token`)",
1049+
"in": "query",
1050+
"name": "user_id",
1051+
"required": false,
1052+
"type": "string"
1053+
},
1054+
{
1055+
"description": "Get the quota limit by user email (mutually exclusive with `user_id` and `user_access_token`)",
1056+
"in": "query",
1057+
"name": "email",
1058+
"required": false,
1059+
"type": "string"
1060+
},
1061+
{
1062+
"description": "Get the quota limit by user access token (mutually exclusive with `user_id` and `email`)",
1063+
"in": "query",
1064+
"name": "user_access_token",
1065+
"required": false,
1066+
"type": "string"
1067+
},
1068+
{
1069+
"description": "The type of resource",
1070+
"in": "query",
1071+
"name": "resource_type",
1072+
"required": true,
1073+
"type": "string"
1074+
}
1075+
],
1076+
"produces": [
1077+
"application/json"
1078+
],
1079+
"responses": {
1080+
"200": {
1081+
"description": "Request succeeded. Raw resource quota limit is returned.",
1082+
"examples": {
1083+
"application/json": {
1084+
"data": 1000000000,
1085+
"message": "",
1086+
"status": "200"
1087+
}
1088+
},
1089+
"schema": {
1090+
"properties": {
1091+
"data": {
1092+
"type": "number"
1093+
},
1094+
"message": {
1095+
"type": "string"
1096+
},
1097+
"status": {
1098+
"type": "string"
1099+
}
1100+
},
1101+
"type": "object"
1102+
}
1103+
},
1104+
"400": {
1105+
"description": "Request failed. The incoming data specification seems malformed.",
1106+
"examples": {
1107+
"application/json": {
1108+
"message": "No user specified."
1109+
}
1110+
},
1111+
"schema": {
1112+
"properties": {
1113+
"message": {
1114+
"type": "string"
1115+
}
1116+
},
1117+
"type": "object"
1118+
}
1119+
},
1120+
"404": {
1121+
"description": "Request failed. Not found.",
1122+
"examples": {
1123+
"application/json": {
1124+
"message": "Quota functionality is not enabled."
1125+
}
1126+
},
1127+
"schema": {
1128+
"properties": {
1129+
"message": {
1130+
"type": "string"
1131+
}
1132+
},
1133+
"type": "object"
1134+
}
1135+
},
1136+
"500": {
1137+
"description": "Request failed. Internal server error.",
1138+
"examples": {
1139+
"application/json": {
1140+
"message": "Internal server error."
1141+
}
1142+
},
1143+
"schema": {
1144+
"properties": {
1145+
"message": {
1146+
"type": "string"
1147+
}
1148+
},
1149+
"type": "object"
1150+
}
1151+
}
1152+
},
1153+
"summary": "Get resource quota limits."
1154+
},
1155+
"post": {
1156+
"description": "This endpoint sets resource quota limits for a given set of users.",
1157+
"operationId": "set_quota_limit",
1158+
"parameters": [
1159+
{
1160+
"description": "REANA access token.",
1161+
"in": "query",
1162+
"name": "access_token",
1163+
"required": true,
1164+
"type": "string"
1165+
},
1166+
{
1167+
"description": "Data required to set quota limits.",
1168+
"in": "body",
1169+
"name": "data",
1170+
"required": true,
1171+
"schema": {
1172+
"additionalProperties": {
1173+
"description": "Quota limit definition.",
1174+
"properties": {
1175+
"emails": {
1176+
"description": "Emails of the target users (mutually exclusive with `user_ids`)",
1177+
"type": "string"
1178+
},
1179+
"limit": {
1180+
"description": "Raw quota limit to set",
1181+
"type": "integer"
1182+
},
1183+
"resource_type": {
1184+
"description": "Resource type to set",
1185+
"type": "string"
1186+
},
1187+
"user_ids": {
1188+
"description": "IDs of the target users (mutually exclusive with `emails`)",
1189+
"type": "string"
1190+
}
1191+
},
1192+
"type": "object"
1193+
},
1194+
"type": "object"
1195+
}
1196+
}
1197+
],
1198+
"produces": [
1199+
"application/json"
1200+
],
1201+
"responses": {
1202+
"200": {
1203+
"description": "Resource quotas successfully set.",
1204+
"examples": {
1205+
"application/json": {
1206+
"message": "Quota limit {limit} for '{resource_type} ({resource_name})' successfully set to user(s) {user_ids}."
1207+
}
1208+
},
1209+
"schema": {
1210+
"properties": {
1211+
"message": {
1212+
"type": "string"
1213+
}
1214+
},
1215+
"type": "object"
1216+
}
1217+
},
1218+
"500": {
1219+
"description": "Request failed. Internal server error.",
1220+
"examples": {
1221+
"application/json": {
1222+
"message": "Quota could not be set: {error}"
1223+
}
1224+
},
1225+
"schema": {
1226+
"properties": {
1227+
"message": {
1228+
"type": "string"
1229+
}
1230+
},
1231+
"type": "object"
1232+
}
1233+
}
1234+
},
1235+
"summary": "Set resource quota limits."
1236+
}
1237+
},
10351238
"/api/secrets": {
10361239
"get": {
10371240
"description": "Get user secrets.",

modules/reana-commons

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit d009e4474d94be7e0a3411801eae9cc0007acf1c

modules/reana-db

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit b13bb3bae824ee2eb39aa8328bbda111c1b80f75

reana_server/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@
9999
)
100100
"""Maximum number of threads for one Dask worker."""
101101

102+
REANA_ADMIN_QUOTA_MANAGER = os.getenv("REANA_ADMIN_QUOTA_MANAGER", "")
103+
"""Secret used to authenticate the admin user when setting quota limits."""
104+
102105
REANA_KUBERNETES_JOBS_MEMORY_LIMIT = os.getenv("REANA_KUBERNETES_JOBS_MEMORY_LIMIT")
103106
"""Maximum memory limit for user job containers for workflow complexity estimation."""
104107

reana_server/factory.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def create_minimal_app(config_mapping=None):
7272
workflows,
7373
info,
7474
launch,
75+
quota,
7576
) # noqa
7677

7778
app.register_blueprint(ping.blueprint, url_prefix="/api")
@@ -83,6 +84,7 @@ def create_minimal_app(config_mapping=None):
8384
app.register_blueprint(status.blueprint, url_prefix="/api")
8485
app.register_blueprint(info.blueprint, url_prefix="/api")
8586
app.register_blueprint(launch.blueprint, url_prefix="/api")
87+
app.register_blueprint(quota.blueprint, url_prefix="/api")
8688

8789
@app.teardown_appcontext
8890
def shutdown_session(response_or_exc):

reana_server/reana_admin/cli.py

Lines changed: 11 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
AuditLogAction,
3838
QuotaHealth,
3939
Resource,
40-
ResourceType,
4140
User,
4241
UserResource,
4342
UserTokenStatus,
@@ -67,6 +66,7 @@
6766
_validate_email,
6867
_validate_password,
6968
create_user_workspace,
69+
_set_quota_limit,
7070
)
7171

7272

@@ -565,76 +565,17 @@ def set_quota_limit(
565565
ctx, emails, resource_type, resource_name, limit, admin_access_token
566566
):
567567
"""Set quota limits to the given users per resource."""
568-
try:
569-
for email in emails:
570-
error_msg = None
571-
resource = None
572-
user = _get_user_by_criteria(None, email)
573-
574-
if resource_name:
575-
resource = Resource.query.filter_by(name=resource_name).one_or_none()
576-
elif resource_type in ResourceType._member_names_:
577-
resources = Resource.query.filter_by(type_=resource_type).all()
578-
if resources and len(resources) > 1:
579-
click.secho(
580-
f"ERROR: There are more than one `{resource_type}` resource. "
581-
"Please provide resource name with `--resource-name` option to specify the exact resource.",
582-
fg="red",
583-
err=True,
584-
)
585-
sys.exit(1)
586-
else:
587-
resource = resources[0]
588-
589-
if not user:
590-
error_msg = f"ERROR: Provided user {email} does not exist."
591-
elif not resource:
592-
resources = [
593-
f"{resource.type_.name} ({resource.name})"
594-
for resource in Resource.query
595-
]
596-
error_msg = (
597-
f"ERROR: Provided resource `{resource_name or resource_type}` does not exist. "
598-
if resource_name or resource_type
599-
else "ERROR: Please provide a resource. "
600-
)
601-
error_msg += f"Available resources are: {', '.join(resources)}."
602-
if error_msg:
603-
click.secho(
604-
error_msg,
605-
fg="red",
606-
err=True,
607-
)
608-
sys.exit(1)
568+
msg, status_code, fatal = _set_quota_limit(
569+
resource_type, resource_name, limit, emails=emails
570+
)
571+
click.secho(
572+
msg,
573+
fg="green" if status_code == 200 else "red",
574+
err=False if status_code == 200 else True,
575+
)
609576

610-
user_resource = UserResource.query.filter_by(
611-
user=user, resource=resource
612-
).one_or_none()
613-
if user_resource:
614-
user_resource.quota_limit = limit
615-
Session.add(user_resource)
616-
else:
617-
# Create user resource in case there isn't one. Useful for old users.
618-
user.resources.append(
619-
UserResource(
620-
user_id=user.id_,
621-
resource_id=resource.id_,
622-
quota_limit=limit,
623-
quota_used=0,
624-
)
625-
)
626-
Session.commit()
627-
click.secho(
628-
f"Quota limit {limit} for '{resource.type_.name} ({resource.name})' successfully set to users {emails}.",
629-
fg="green",
630-
)
631-
except Exception as e:
632-
logging.debug(traceback.format_exc())
633-
logging.debug(str(e))
634-
click.echo(
635-
click.style("Quota could not be set: \n{}".format(str(e)), fg="red"),
636-
err=True,
637-
)
577+
if fatal:
578+
sys.exit(1)
638579

639580

640581
@reana_admin.command(

0 commit comments

Comments
 (0)