Skip to content

Commit

Permalink
frontend: migrate monitor, modules, mock_chroots, webhooks enpoints t…
Browse files Browse the repository at this point in the history
…o restx
  • Loading branch information
nikromen committed Feb 6, 2024
1 parent 43dba77 commit 8698c36
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 117 deletions.
23 changes: 20 additions & 3 deletions frontend/coprs_frontend/coprs/views/apiv3_ns/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,18 @@ def pagination_wrapper(*args, **kwargs):
return pagination_decorator


def _shared_file_upload_wrapper():
data = json.loads(flask.request.files["json"].read()) or {}
tuples = [(k, v) for k, v in data.items()]
flask.request.form = ImmutableMultiDict(tuples)


def file_upload():
def file_upload_decorator(f):
@wraps(f)
def file_upload_wrapper(*args, **kwargs):
if "json" in flask.request.files:
data = json.loads(flask.request.files["json"].read()) or {}
tuples = [(k, v) for k, v in data.items()]
flask.request.form = ImmutableMultiDict(tuples)
_shared_file_upload_wrapper()
return f(*args, **kwargs)
return file_upload_wrapper
return file_upload_decorator
Expand Down Expand Up @@ -506,3 +510,16 @@ def create_pagination(self, *args, **kwargs):
kwargs = _shared_pagination_wrapper(**kwargs)
return endpoint_method(self, *args, **kwargs)
return create_pagination


def restx_file_upload(endpoint_method):
"""
Allow uploading a file to a form via endpoint by using this function as an endpoint decorator.
"""
@wraps(endpoint_method)
def inner(self, *args, **kwargs):
if "json" in flask.request.files:
_shared_file_upload_wrapper()
return endpoint_method(self, *args, **kwargs)
return inner

43 changes: 31 additions & 12 deletions frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_mock_chroots.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
import flask
from http import HTTPStatus

from flask_restx import Namespace, Resource
from flask_restx.fields import String
from html2text import html2text
from coprs.views.apiv3_ns import apiv3_ns

from coprs.views.apiv3_ns import api
from coprs.logic.coprs_logic import MockChrootsLogic


@apiv3_ns.route("/mock-chroots/list")
def list_chroots():
chroots = MockChrootsLogic.active_names_with_comments()
response = {}
for chroot, comment in chroots:
if comment:
response[chroot] = html2text(comment).strip("\n")
else:
response[chroot] = ""
apiv3_mock_chroots_ns = Namespace("mock-chroots", description="Mock chroots")
api.add_namespace(apiv3_mock_chroots_ns)


# this model is so tiny, ez and generic it's not worth it defining in our schema.py
mock_chroot_model = api.model("MockChroot", {String: String})


@apiv3_mock_chroots_ns.route("/list")
class MockChroot(Resource):
@apiv3_mock_chroots_ns.marshal_with(mock_chroot_model)
@apiv3_mock_chroots_ns.response(HTTPStatus.OK.value, "OK, Mock chroot data follows...")
def get(self):
"""
Get list of mock chroots
Get list of all currently active mock chroots with additional comment.
"""
chroots = MockChrootsLogic.active_names_with_comments()
response = {}
for chroot, comment in chroots:
if comment:
response[chroot] = html2text(comment).strip("\n")
else:
response[chroot] = ""

return flask.jsonify(response)
return response
77 changes: 49 additions & 28 deletions frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_modules.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,64 @@
import flask
import sqlalchemy

from http import HTTPStatus
from flask_restx import Namespace, Resource
from requests.exceptions import RequestException, InvalidSchema
from wtforms import ValidationError

from coprs import forms, db_session_scope
from coprs.views.apiv3_ns import apiv3_ns, get_copr, file_upload, POST
from coprs.views.misc import api_login_required
from coprs.views.apiv3_ns import api, get_copr, restx_file_upload
from coprs.views.apiv3_ns.schema.schemas import module_build_model, fullname_params
from coprs.views.misc import restx_api_login_required, restx_file_upload
from coprs.exceptions import DuplicateException, BadRequest, InvalidForm
from coprs.logic.modules_logic import ModuleProvider, ModuleBuildFacade


apiv3_module_ns = Namespace("module", description="Module")
api.add_namespace(apiv3_module_ns)


def to_dict(module):
return {
"nsv": module.nsv,
}


@apiv3_ns.route("/module/build/<ownername>/<projectname>", methods=POST)
@api_login_required
@file_upload()
def build_module(ownername, projectname):
copr = get_copr(ownername, projectname)
form = forms.get_module_build_form(meta={'csrf': False})
if not form.validate_on_submit():
raise InvalidForm(form)

facade = None
try:
mod_info = ModuleProvider.from_input(form.modulemd.data or form.scmurl.data)
facade = ModuleBuildFacade(flask.g.user, copr, mod_info.yaml,
mod_info.filename, form.distgit.data)
with db_session_scope():
module = facade.submit_build()
return flask.jsonify(to_dict(module))

except (ValidationError, RequestException, InvalidSchema, RuntimeError) as ex:
raise BadRequest(str(ex)) from ex

except sqlalchemy.exc.IntegrityError as err:
raise DuplicateException("Module {}-{}-{} already exists"
.format(facade.modulemd.get_module_name(),
facade.modulemd.get_stream_name(),
facade.modulemd.get_version())) from err
@apiv3_module_ns.route("/build/<ownername>/<projectname>")
class Module(Resource):
# TODO: what data needs to be in expect?
@restx_api_login_required
@restx_file_upload
@apiv3_module_ns.doc(params=fullname_params)
@apiv3_module_ns.marshal_with(module_build_model)
@apiv3_module_ns.response(HTTPStatus.OK.value, "Module build successfully submitted")
@apiv3_module_ns.response(
HTTPStatus.BAD_REQUEST.value, HTTPStatus.BAD_REQUEST.description
)
def post(self, ownername, projectname):
"""
Create a module build
Create a module build for ownername/projectname project.
"""
copr = get_copr(ownername, projectname)
form = forms.get_module_build_form(meta={'csrf': False})
if not form.validate_on_submit():
raise InvalidForm(form)

facade = None
try:
mod_info = ModuleProvider.from_input(form.modulemd.data or form.scmurl.data)
facade = ModuleBuildFacade(flask.g.user, copr, mod_info.yaml,
mod_info.filename, form.distgit.data)
with db_session_scope():
module = facade.submit_build()
return to_dict(module)

except (ValidationError, RequestException, InvalidSchema, RuntimeError) as ex:
raise BadRequest(str(ex)) from ex

except sqlalchemy.exc.IntegrityError as err:
raise DuplicateException("Module {}-{}-{} already exists"
.format(facade.modulemd.get_module_name(),
facade.modulemd.get_stream_name(),
facade.modulemd.get_version())) from err
128 changes: 70 additions & 58 deletions frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_monitor.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
"""
/api_3/monitor routes
"""
from http import HTTPStatus

import flask

from flask_restx import Namespace, Resource

from coprs.exceptions import BadRequest
from coprs.logic.builds_logic import BuildsMonitorLogic
from coprs.logic.coprs_logic import CoprDirsLogic
from coprs.views.apiv3_ns import (
apiv3_ns,
GET,
get_copr,
query_params,
streamed_json_array_response,
)

from coprs.views.apiv3_ns import api, get_copr, streamed_json_array_response, query_to_parameters
from coprs.views.apiv3_ns.schema.schemas import fullname_params
from coprs.measure import checkpoint


apiv3_monitor_ns = Namespace("monitor", description="Monitor")
api.add_namespace(apiv3_monitor_ns)


def monitor_generator(copr_dir, additional_fields):
"""
Continuosly fill-up the package_monitor() buffer.
Expand Down Expand Up @@ -48,54 +50,64 @@ def monitor_generator(copr_dir, additional_fields):
checkpoint("Last package queried")


@apiv3_ns.route("/monitor", methods=GET)
@query_params()
def package_monitor(ownername, projectname, project_dirname=None):
"""
For list of the project packages return list of JSON dictionaries informing
about status of the last chroot builds (status, build log, etc.).
"""
checkpoint("API3 monitor start")

additional_fields = flask.request.args.getlist("additional_fields[]")

copr = get_copr(ownername, projectname)

valid_additional_fields = [
"url_build_log",
"url_backend_log",
"url_build",
]

if additional_fields:
additional_fields = set(additional_fields)
bad_fields = []
for field in sorted(additional_fields):
if field not in valid_additional_fields:
bad_fields += [field]
if bad_fields:
raise BadRequest(
"Wrong additional_fields argument(s): " +
", ".join(bad_fields)
@apiv3_monitor_ns.route("")
class Monitor(Resource):
# TODO: marshalling - will streamed_json_array_response work?
@query_to_parameters
@apiv3_monitor_ns.doc(params=fullname_params)
@apiv3_monitor_ns.response(
HTTPStatus.PARTIAL_CONTENT.value, HTTPStatus.PARTIAL_CONTENT.description
)
@apiv3_monitor_ns.response(
HTTPStatus.BAD_REQUEST.value, HTTPStatus.BAD_REQUEST.description
)
def get(self, ownername, projectname, project_dirname=None):
"""
Get info about builds
For list of the project packages return list of JSON dictionaries informing
about status of the last chroot builds (status, build log, etc.).
"""
checkpoint("API3 monitor start")

additional_fields = flask.request.args.getlist("additional_fields[]")

copr = get_copr(ownername, projectname)

valid_additional_fields = [
"url_build_log",
"url_backend_log",
"url_build",
]

if additional_fields:
additional_fields = set(additional_fields)
bad_fields = []
for field in sorted(additional_fields):
if field not in valid_additional_fields:
bad_fields += [field]
if bad_fields:
raise BadRequest(
"Wrong additional_fields argument(s): " +
", ".join(bad_fields)
)
else:
additional_fields = set()

if project_dirname:
copr_dir = CoprDirsLogic.get_by_copr(copr, project_dirname)
else:
copr_dir = copr.main_dir

# Preload those to avoid the error sqlalchemy.orm.exc.DetachedInstanceError
# http://sqlalche.me/e/13/bhk3
_ = copr_dir.copr.active_chroots
_ = copr_dir.copr.group

try:
return streamed_json_array_response(
monitor_generator(copr_dir, additional_fields),
"Project monitor request successful",
"packages",
)
else:
additional_fields = set()

if project_dirname:
copr_dir = CoprDirsLogic.get_by_copr(copr, project_dirname)
else:
copr_dir = copr.main_dir

# Preload those to avoid the error sqlalchemy.orm.exc.DetachedInstanceError
# http://sqlalche.me/e/13/bhk3
_ = copr_dir.copr.active_chroots
_ = copr_dir.copr.group

try:
return streamed_json_array_response(
monitor_generator(copr_dir, additional_fields),
"Project monitor request successful",
"packages",
)
finally:
checkpoint("Streaming prepared")
finally:
checkpoint("Streaming prepared")
45 changes: 29 additions & 16 deletions frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_webhooks.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
"""
APIv3 endpoints related to webhooks
"""
from http import HTTPStatus

from flask_restx import Namespace, Resource

import flask
from coprs import db
from coprs.views.misc import api_login_required
from coprs.views.apiv3_ns import editable_copr, POST
from coprs.views.apiv3_ns import apiv3_ns
from coprs.views.misc import restx_api_login_required
from coprs.views.apiv3_ns import api, restx_editable_copr
from coprs.views.apiv3_ns.schema.schemas import fullname_params, webhook_secret_model


apiv3_webhooks_ns = Namespace("webhook", description="Webhooks")
api.add_namespace(apiv3_webhooks_ns)


def to_dict(copr):
Expand All @@ -23,15 +29,22 @@ def to_dict(copr):
}


@apiv3_ns.route("/webhook/generate/<ownername>/<projectname>", methods=POST)
@api_login_required
@editable_copr
def new_webhook_secret(copr):
"""
Generate a new webhook secret for a given project.
Not an additional secret, though. The previous secret gets lost.
"""
copr.new_webhook_secret()
db.session.add(copr)
db.session.commit()
return flask.jsonify(to_dict(copr))
@apiv3_webhooks_ns.route("/generate/<ownername>/<projectname>")
class WebhookSecret(Resource):
@restx_api_login_required
@restx_editable_copr
@apiv3_webhooks_ns.doc(params=fullname_params)
@apiv3_webhooks_ns.marshal_with(webhook_secret_model)
@apiv3_webhooks_ns.response(HTTPStatus.OK.value, "Webhook secret created")
@apiv3_webhooks_ns.response(
HTTPStatus.BAD_REQUEST.value, HTTPStatus.BAD_REQUEST.description
)
def post(self, copr):
"""
Generate a new webhook secret for a given project.
Not an additional secret, though. The previous secret gets lost.
"""
copr.new_webhook_secret()
db.session.add(copr)
db.session.commit()
return to_dict(copr)
Loading

0 comments on commit 8698c36

Please sign in to comment.