From 1cc55a3c0f4c07bc92b0eb3780d6c5b6b4044cf0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 6 Feb 2022 16:43:31 +0100 Subject: [PATCH 01/12] Check only if status is in {FAILED, PASSED} --- lifemonitor/tasks/tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lifemonitor/tasks/tasks.py b/lifemonitor/tasks/tasks.py index f5e0ab4c3..9a4cbd2b3 100644 --- a/lifemonitor/tasks/tasks.py +++ b/lifemonitor/tasks/tasks.py @@ -110,6 +110,8 @@ def check_last_build(): # check state transition failed = last_build.status == BuildStatus.FAILED if len(builds) == 1 and failed or \ + builds[0].status in (BuildStatus.FAILED, BuildStatus.PASSED) and \ + builds[1].status in (BuildStatus.FAILED, BuildStatus.PASSED) and \ len(builds) > 1 and builds[1].status != last_build.status: logger.info("Updating latest build: %r", last_build) notification_name = f"{last_build} {'FAILED' if failed else 'RECOVERED'}" From 93e95ef72c53a702f34c2edc7b26cb5bda8a74c8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 8 Feb 2022 17:41:28 +0100 Subject: [PATCH 02/12] Enable inheritance of Notification model --- lifemonitor/auth/models.py | 41 +++++++++++++++++++ ...llow_to_differentiate_notifications_by_.py | 26 ++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 migrations/versions/7e9e009f4dbb_allow_to_differentiate_notifications_by_.py diff --git a/lifemonitor/auth/models.py b/lifemonitor/auth/models.py index a460c5b55..9782768df 100644 --- a/lifemonitor/auth/models.py +++ b/lifemonitor/auth/models.py @@ -480,10 +480,16 @@ class Notification(db.Model, ModelMixin): name = db.Column("name", db.String, nullable=True, index=True) _event = db.Column("event", db.Integer, nullable=False) _data = db.Column("data", JSON, nullable=True) + _type = db.Column("type", db.String, nullable=False) users: List[UserNotification] = db.relationship("UserNotification", back_populates="notification", cascade="all, delete-orphan") + __mapper_args__ = { + 'polymorphic_on': _type, + 'polymorphic_identity': 'generic' + } + def __init__(self, event: EventType, name: str, data: object, users: List[User]) -> None: self.name = name self._event = event.value @@ -491,6 +497,13 @@ def __init__(self, event: EventType, name: str, data: object, users: List[User]) for u in users: self.add_user(u) + def __repr__(self) -> str: + return f"{self.__class__.__name__} ({self.id})" + + @property + def reply_to(self) -> str: + return "noreply-lifemonitor@crs4.it" + @property def event(self) -> EventType: return EventType(self._event) @@ -506,6 +519,25 @@ def add_user(self, user: User): def remove_user(self, user: User): self.users.remove(user) + def to_mail_message(self, recipients: List[User]) -> str: + return None + + @property + def base64Logo(self) -> str: + try: + return lm_utils.Base64Encoder.encode_file('lifemonitor/static/img/logo/lm/LifeMonitorLogo.png') + except Exception as e: + logger.debug(e) + return None + + @staticmethod + def encodeFile(file_path: str) -> str: + try: + return lm_utils.Base64Encoder.encode_file(file_path) + except Exception as e: + logger.debug(e) + return None + @classmethod def find_by_name(cls, name: str) -> List[Notification]: return cls.query.filter(cls.name == name).all() @@ -520,6 +552,15 @@ def not_emailed(cls) -> List[Notification]: return cls.query.join(UserNotification, UserNotification.notification_id == cls.id)\ .filter(UserNotification.emailed == null()).all() + @classmethod + def older_than(cls, date: datetime) -> List[Notification]: + return cls.query.filter(Notification.created < date).all() + + @classmethod + def find_by_user(cls, user: User) -> List[Notification]: + return cls.query.join(UserNotification, UserNotification.notification_id == cls.id)\ + .filter(UserNotification.user_id == user.id).all() + class UserNotification(db.Model): diff --git a/migrations/versions/7e9e009f4dbb_allow_to_differentiate_notifications_by_.py b/migrations/versions/7e9e009f4dbb_allow_to_differentiate_notifications_by_.py new file mode 100644 index 000000000..a20e3e79a --- /dev/null +++ b/migrations/versions/7e9e009f4dbb_allow_to_differentiate_notifications_by_.py @@ -0,0 +1,26 @@ +"""Allow to differentiate notifications by type + +Revision ID: 7e9e009f4dbb +Revises: 505e4e6976de +Create Date: 2022-02-07 14:21:52.319351 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '7e9e009f4dbb' +down_revision = '505e4e6976de' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('notification', sa.Column('type', sa.String(), nullable=True)) + op.execute("UPDATE notification SET type = 'workflow_status_notification'") + op.alter_column('notification', 'type', nullable=False) + + +def downgrade(): + op.drop_column('notification', 'type') From 5755f6228b046055bd97babdd93cefd595d45bd3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 8 Feb 2022 17:47:18 +0100 Subject: [PATCH 03/12] Refactor workflow status notifications --- lifemonitor/api/models/__init__.py | 6 +- lifemonitor/api/models/notifications.py | 85 +++++++++++++++++++++++++ lifemonitor/mail.py | 43 +++---------- lifemonitor/tasks/tasks.py | 10 +-- 4 files changed, 105 insertions(+), 39 deletions(-) create mode 100644 lifemonitor/api/models/notifications.py diff --git a/lifemonitor/api/models/__init__.py b/lifemonitor/api/models/__init__.py index a3a299ac3..5e87936d6 100644 --- a/lifemonitor/api/models/__init__.py +++ b/lifemonitor/api/models/__init__.py @@ -38,6 +38,9 @@ # 'testsuites' package from .testsuites import TestSuite, TestInstance, ManagedTestInstance, BuildStatus, TestBuild +# notifications +from .notifications import WorkflowStatusNotification + # 'testing_services' from .services import TestingService, \ GithubTestingService, GithubTestBuild, \ @@ -71,8 +74,9 @@ "WorkflowRegistry", "WorkflowRegistryClient", "WorkflowStatus", + "WorkflowStatusNotification", "WorkflowVersion", - "RegistryWorkflow" + "RegistryWorkflow", ] # set module level logger diff --git a/lifemonitor/api/models/notifications.py b/lifemonitor/api/models/notifications.py new file mode 100644 index 000000000..a7d23841b --- /dev/null +++ b/lifemonitor/api/models/notifications.py @@ -0,0 +1,85 @@ +# Copyright (c) 2020-2021 CRS4 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import logging +from typing import List + +from flask import render_template +from flask_mail import Message +from lifemonitor.auth.models import (EventType, Notification, User, + UserNotification) +from lifemonitor.db import db +from lifemonitor.utils import Base64Encoder +from sqlalchemy.exc import InternalError + +from . import TestInstance + +# set logger +logger = logging.getLogger(__name__) + + +class WorkflowStatusNotification(Notification): + + __mapper_args__ = { + 'polymorphic_identity': 'workflow_status_notification' + } + + def get_icon_path(self) -> str: + return 'lifemonitor/static/img/icons/' \ + + ('times-circle-solid.svg' + if self.event == EventType.BUILD_FAILED else 'check-circle-solid.svg') + + def to_mail_message(self, recipients: List[User]) -> Message: + from lifemonitor.mail import mail + build_data = self.data['build'] + try: + i = TestInstance.find_by_uuid(build_data['instance']['uuid']) + if i is not None: + wv = i.test_suite.workflow_version + b = i.get_test_build(build_data['build_id']) + suite = i.test_suite + suite.url_param = Base64Encoder.encode_object({ + 'workflow': str(wv.workflow.uuid), + 'suite': str(suite.uuid) + }) + instance_status = "is failing" \ + if self.event == EventType.BUILD_FAILED else "has recovered" + msg = Message( + f'Workflow "{wv.name} ({wv.version})": test instance {i.name} {instance_status}', + bcc=recipients, + reply_to=self.reply_to + ) + msg.html = render_template("mail/instance_status_notification.j2", + webapp_url=mail.webapp_url, + workflow_version=wv, build=b, + test_instance=i, + suite=suite, + json_data=build_data, + logo=self.base64Logo, icon=self.encodeFile(self.get_icon_path)) + return msg + except InternalError as e: + logger.debug(e) + db.session.rollback() + + @classmethod + def find_by_user(cls, user: User) -> List[Notification]: + return cls.query.join(UserNotification, UserNotification.notification_id == cls.id)\ + .filter(UserNotification.user_id == user.id).all() diff --git a/lifemonitor/mail.py b/lifemonitor/mail.py index 8f4404cda..6a353367b 100644 --- a/lifemonitor/mail.py +++ b/lifemonitor/mail.py @@ -70,46 +70,21 @@ def send_email_validation_message(user: User): conn.send(msg) -def send_notification(n: Notification, recipients: List[str]) -> Optional[datetime]: +def send_notification(n: Notification, recipients: List[str] = None) -> Optional[datetime]: if mail.disabled: logger.info("Mail notifications are disabled") else: with mail.connect() as conn: logger.debug("Mail recipients for notification '%r': %r", n.id, recipients) + if not recipients: + recipients = [ + u.user.email for u in n.users + if u.emailed is None and u.user.email_notifications_enabled and u.user.email + ] if len(recipients) > 0: - build_data = n.data['build'] try: - i = TestInstance.find_by_uuid(build_data['instance']['uuid']) - if i is not None: - wv = i.test_suite.workflow_version - b = i.get_test_build(build_data['build_id']) - suite = i.test_suite - logo = Base64Encoder.encode_file('lifemonitor/static/img/logo/lm/LifeMonitorLogo.png') - icon_path = 'lifemonitor/static/img/icons/' \ - + ('times-circle-solid.svg' - if n.event == EventType.BUILD_FAILED else 'check-circle-solid.svg') - icon = Base64Encoder.encode_file(icon_path) - suite.url_param = Base64Encoder.encode_object({ - 'workflow': str(wv.workflow.uuid), - 'suite': str(suite.uuid) - }) - instance_status = "is failing" \ - if n.event == EventType.BUILD_FAILED else "has recovered" - msg = Message( - f'Workflow "{wv.name} ({wv.version})": test instance {i.name} {instance_status}', - bcc=recipients, - reply_to="noreply-lifemonitor@crs4.it" - ) - msg.html = render_template("mail/instance_status_notification.j2", - webapp_url=mail.webapp_url, - workflow_version=wv, build=b, - test_instance=i, - suite=suite, - json_data=build_data, - logo=logo, icon=icon) - conn.send(msg) - return datetime.utcnow() - except InternalError as e: + conn.send(n.to_mail_message(recipients)) + return datetime.utcnow() + except Exception as e: logger.debug(e) - db.session.rollback() return None diff --git a/lifemonitor/tasks/tasks.py b/lifemonitor/tasks/tasks.py index 9a4cbd2b3..70ddedf10 100644 --- a/lifemonitor/tasks/tasks.py +++ b/lifemonitor/tasks/tasks.py @@ -6,6 +6,7 @@ import flask from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.interval import IntervalTrigger +from lifemonitor.api.models.notifications import WorkflowStatusNotification from lifemonitor.api.models.testsuites.testbuild import BuildStatus from lifemonitor.api.serializers import BuildSummarySchema from lifemonitor.auth.models import EventType, Notification @@ -117,10 +118,11 @@ def check_last_build(): notification_name = f"{last_build} {'FAILED' if failed else 'RECOVERED'}" if len(Notification.find_by_name(notification_name)) == 0: users = latest_version.workflow.get_subscribers() - n = Notification(EventType.BUILD_FAILED if failed else EventType.BUILD_RECOVERED, - notification_name, - {'build': BuildSummarySchema(exclude_nested=False).dump(last_build)}, - users) + n = WorkflowStatusNotification( + EventType.BUILD_FAILED if failed else EventType.BUILD_RECOVERED, + notification_name, + {'build': BuildSummarySchema(exclude_nested=False).dump(last_build)}, + users) n.save() except Exception as e: logger.error("Error when executing task 'check_last_build': %s", str(e)) From 5f229448c51088c389db7737e46ed025fa0f38c6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 8 Feb 2022 17:53:06 +0100 Subject: [PATCH 04/12] Add support for notifications of unconfigured email address --- lifemonitor/auth/models.py | 16 ++++++++++++++++ lifemonitor/tasks/tasks.py | 29 ++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/lifemonitor/auth/models.py b/lifemonitor/auth/models.py index 9782768df..2670c09f6 100644 --- a/lifemonitor/auth/models.py +++ b/lifemonitor/auth/models.py @@ -323,6 +323,7 @@ class EventType(Enum): ALL = 0 BUILD_FAILED = 1 BUILD_RECOVERED = 2 + UNCONFIGURED_EMAIL = 3 @classmethod def all(cls): @@ -562,6 +563,21 @@ def find_by_user(cls, user: User) -> List[Notification]: .filter(UserNotification.user_id == user.id).all() +class UnconfiguredEmailNotification(Notification): + + __mapper_args__ = { + 'polymorphic_identity': 'unconfigured_email' + } + + def __init__(self, name: str, data: object = None, users: List[User] = None) -> None: + super().__init__(EventType.UNCONFIGURED_EMAIL, name, data, users) + + @classmethod + def find_by_user(cls, user: User) -> List[Notification]: + return cls.query.join(UserNotification, UserNotification.notification_id == cls.id)\ + .filter(UserNotification.user_id == user.id).all() + + class UserNotification(db.Model): emailed = db.Column(db.DateTime, default=None, nullable=True) diff --git a/lifemonitor/tasks/tasks.py b/lifemonitor/tasks/tasks.py index 70ddedf10..701a8e99e 100644 --- a/lifemonitor/tasks/tasks.py +++ b/lifemonitor/tasks/tasks.py @@ -9,7 +9,8 @@ from lifemonitor.api.models.notifications import WorkflowStatusNotification from lifemonitor.api.models.testsuites.testbuild import BuildStatus from lifemonitor.api.serializers import BuildSummarySchema -from lifemonitor.auth.models import EventType, Notification +from lifemonitor.auth.models import (EventType, Notification, + UnconfiguredEmailNotification, User) from lifemonitor.cache import Timeout from lifemonitor.mail import send_notification @@ -141,9 +142,9 @@ def send_email_notifications(): logger.debug("Processing notification %r ...", n) recipients = [ u.user.email for u in n.users - if u.emailed is None and u.user.email_notifications_enabled and u.user.email is not None + if u.emailed is None and u.user.email_notifications_enabled and u.user.email ] - sent = send_notification(n, recipients) + sent = send_notification(n, recipients=recipients) logger.debug("Notification email sent: %r", sent is not None) if sent: logger.debug("Notification '%r' sent by email @ %r", n.id, sent) @@ -172,3 +173,25 @@ def cleanup_notifications(): logger.debug(e) logger.error("Error when deleting notification %r", n) logger.info("Notification cleanup completed: deleted %r notifications", count) + + +@schedule(IntervalTrigger(seconds=60)) +@dramatiq.actor(max_retries=0, max_age=TASK_EXPIRATION_TIME) +def check_email_configuration(): + logger.info("Check for users without notification email") + count = 0 + users = [] + try: + for u in User.all(): + if not u.email and len(UnconfiguredEmailNotification.find_by_user(u)) == 0: + users.append(u) + count += 1 + if len(users) > 0: + n = UnconfiguredEmailNotification( + "Unconfigured email", + users=users) + n.save() + except Exception as e: + logger.debug(e) + logger.error("Error when deleting notification %r", n) + logger.info("Check for users without notification email configured: generated notification for %r users", count) From ed4ed7c16ec1ccb1b20b4eaba8fb1bd228fbbdd5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 8 Feb 2022 17:56:18 +0100 Subject: [PATCH 05/12] Fix unused imports --- lifemonitor/mail.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lifemonitor/mail.py b/lifemonitor/mail.py index 6a353367b..308887b83 100644 --- a/lifemonitor/mail.py +++ b/lifemonitor/mail.py @@ -25,12 +25,10 @@ from flask import Flask, render_template from flask_mail import Mail, Message -from sqlalchemy.exc import InternalError -from lifemonitor.api.models import TestInstance -from lifemonitor.auth.models import EventType, Notification, User -from lifemonitor.db import db -from lifemonitor.utils import Base64Encoder, get_external_server_url, boolean_value +from lifemonitor.auth.models import Notification, User +from lifemonitor.utils import (Base64Encoder, boolean_value, + get_external_server_url) # set logger logger = logging.getLogger(__name__) From 2963fc8d4b300ebd3ebc18c97559565343364384 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 9 Feb 2022 15:15:06 +0100 Subject: [PATCH 06/12] Don't send UNCONFIGURED_EMAIL notification after email address is set --- lifemonitor/tasks/tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lifemonitor/tasks/tasks.py b/lifemonitor/tasks/tasks.py index 701a8e99e..148f3f23b 100644 --- a/lifemonitor/tasks/tasks.py +++ b/lifemonitor/tasks/tasks.py @@ -135,7 +135,8 @@ def check_last_build(): @schedule(IntervalTrigger(seconds=60)) @dramatiq.actor(max_retries=0, max_age=TASK_EXPIRATION_TIME) def send_email_notifications(): - notifications = Notification.not_emailed() + notifications = [n for n in Notification.not_emailed() + if not isinstance(n, UnconfiguredEmailNotification)] logger.info("Found %r notifications to send by email", len(notifications)) count = 0 for n in notifications: From fd7d611ec4ccdac99fbc478f1250d1c08f5210f4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 9 Feb 2022 15:16:13 +0100 Subject: [PATCH 07/12] Add unit test --- lifemonitor/tasks/tasks.py | 1 + tests/unit/tasks/test_notifications.py | 81 ++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 tests/unit/tasks/test_notifications.py diff --git a/lifemonitor/tasks/tasks.py b/lifemonitor/tasks/tasks.py index 148f3f23b..f7be053ba 100644 --- a/lifemonitor/tasks/tasks.py +++ b/lifemonitor/tasks/tasks.py @@ -156,6 +156,7 @@ def send_email_notifications(): count += 1 logger.debug("Processing notification %r ... DONE", n) logger.info("%r notifications sent by email", count) + return count @schedule(CronTrigger(minute=0, hour=1)) diff --git a/tests/unit/tasks/test_notifications.py b/tests/unit/tasks/test_notifications.py new file mode 100644 index 000000000..5b58acda6 --- /dev/null +++ b/tests/unit/tasks/test_notifications.py @@ -0,0 +1,81 @@ +# Copyright (c) 2020-2021 CRS4 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import logging +from unittest.mock import patch + +from lifemonitor.auth.models import Notification, User, UserNotification + +logger = logging.getLogger(__name__) + + +@patch("lifemonitor.mail.mail") +def test_unconfigured_email_notification(mail, app_settings, app_context, user1): + logger.debug("App settings: %r", app_settings) + app = app_context.app + logger.debug("App: %r", app) + config = app.config + logger.debug("Config: %r", config) + logger.debug(user1) + + # get a reference to the current user + user: User = user1['user'] + + # No notification should exist + notifications = Notification.all() + logger.debug("Notifications before check: %r", notifications) + assert len(notifications) == 0, "Unexpected number of notifications" + + # Check email configuration + from lifemonitor.tasks.tasks import check_email_configuration, send_email_notifications + check_email_configuration() + + # Check if notification for user1 and admin has been generated + notifications = Notification.all() + logger.debug("Notifications after check: %r", notifications) + assert len(notifications) == 1, "Unexpected number of notifications" + + n: Notification = notifications[0] + un: UserNotification = next((_.user for _ in n.users if _.user_id == user.id), None) + assert un is not None, "User1 should be notified" + + # no additional notification should be generated + check_email_configuration() + notifications = Notification.all() + logger.debug("Notifications after check: %r", notifications) + assert len(notifications) == 1, "No additional notification should be generated" + + # try to send notifications via email + # no email should be sent beacause the user has no configured email + sent_notifications = send_email_notifications() + assert sent_notifications == 0, "Unexpected number of sent notifications" + + # update and validate user email + user.email = "user1@lifemonitor.eu" + user.verify_email(user.email_verification_code) + user.enable_email_notifications() + user.save() + + # try to send notifications via email + mail.disabled = False + mail.connect.return_value.__enter__.return_value.name = "TempContext" + sent_notifications = send_email_notifications() + mail.connect.assert_not_called() + assert sent_notifications == 0, "Unexpected number of sent notifications" From 33f2d1a131ec558e67e2df9a6259419300527564 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 9 Feb 2022 22:21:13 +0100 Subject: [PATCH 08/12] Extend API with a PUT method to update suites --- lifemonitor/api/controllers.py | 16 +++++++++++++++ specs/api.yaml | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lifemonitor/api/controllers.py b/lifemonitor/api/controllers.py index 6977cb3db..a8bc6dc55 100644 --- a/lifemonitor/api/controllers.py +++ b/lifemonitor/api/controllers.py @@ -573,6 +573,22 @@ def suites_post(wf_uuid, wf_version, body): return {'wf_uuid': str(suite.uuid)}, 201 +@authorized +def suites_put(suite_uuid, body): + try: + suite = _get_suite_or_problem(suite_uuid) + if isinstance(suite, Response): + return suite + suite.name = body.get('name', suite.name) + suite.save() + clear_cache() + logger.debug("Suite %r updated", suite_uuid) + return connexion.NoContent, 204 + except Exception as e: + return lm_exceptions.report_problem(500, "Internal Error", extra_info={"exception": str(e)}, + detail=messages.unable_to_delete_suite.format(suite_uuid)) + + @authorized def suites_delete(suite_uuid): try: diff --git a/specs/api.yaml b/specs/api.yaml index f45de7ef0..ed2e4c091 100644 --- a/specs/api.yaml +++ b/specs/api.yaml @@ -1086,6 +1086,42 @@ paths: "404": $ref: "#/components/responses/NotFound" + put: + summary: Update a test suite + description: "Update basic information of the specified suite" + x-openapi-router-controller: lifemonitor.api.controllers + operationId: "suites_put" + tags: ["Test Suites"] + security: + - apiKey: ["workflow.write"] + - AuthorizationCodeFlow: ["workflow.write"] + - RegistryClientCredentials: ["workflow.write"] + - RegistryCodeFlow: ["workflow.write"] + parameters: + - $ref: "#/components/parameters/suite_uuid" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: A name for the test suite. + example: Workflow Test Suite + responses: + "204": + description: "Suite updated" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + /suites/{suite_uuid}/status: get: summary: "Get test suite status" From ed3e8ecbbbb993807be1248e633aff00542fd2cf Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 9 Feb 2022 22:23:00 +0100 Subject: [PATCH 09/12] Extend API with a PUT method to update test instances --- lifemonitor/api/controllers.py | 16 +++++++++++++++ specs/api.yaml | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lifemonitor/api/controllers.py b/lifemonitor/api/controllers.py index a8bc6dc55..a2c288575 100644 --- a/lifemonitor/api/controllers.py +++ b/lifemonitor/api/controllers.py @@ -666,6 +666,22 @@ def instances_get_by_id(instance_uuid): else serializers.TestInstanceSchema().dump(response) +@authorized +def instances_put(instance_uuid, body): + try: + instance = _get_instances_or_problem(instance_uuid) + if isinstance(instance, Response): + return instance + instance.name = body.get('name', instance.name) + instance.save() + clear_cache() + logger.debug("Instance %r updated", instance_uuid) + return connexion.NoContent, 204 + except Exception as e: + return lm_exceptions.report_problem(500, "Internal Error", extra_info={"exception": str(e)}, + detail=messages.unable_to_delete_suite.format(instance_uuid)) + + @authorized def instances_delete_by_id(instance_uuid): try: diff --git a/specs/api.yaml b/specs/api.yaml index ed2e4c091..0823cf431 100644 --- a/specs/api.yaml +++ b/specs/api.yaml @@ -1263,6 +1263,42 @@ paths: "404": $ref: "#/components/responses/NotFound" + put: + summary: Update a test instance + description: "Update basic information of the specified test instance" + x-openapi-router-controller: lifemonitor.api.controllers + operationId: "instances_put" + tags: ["Test Instances"] + security: + - apiKey: ["workflow.write"] + - AuthorizationCodeFlow: ["workflow.write"] + - RegistryClientCredentials: ["workflow.write"] + - RegistryCodeFlow: ["workflow.write"] + parameters: + - $ref: "#/components/parameters/instance_uuid" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: A name for the test instance. + example: Workflow Test Instance + responses: + "204": + description: "Test instance updated" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + delete: x-openapi-router-controller: lifemonitor.api.controllers operationId: "instances_delete_by_id" From 61ca2bc06f1e6b7f41a0534c41eca0a239b3ec49 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 9 Feb 2022 22:32:13 +0100 Subject: [PATCH 10/12] Auto remove old notifications after email has been set up --- lifemonitor/tasks/tasks.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lifemonitor/tasks/tasks.py b/lifemonitor/tasks/tasks.py index f7be053ba..532d2e2c2 100644 --- a/lifemonitor/tasks/tasks.py +++ b/lifemonitor/tasks/tasks.py @@ -181,13 +181,17 @@ def cleanup_notifications(): @dramatiq.actor(max_retries=0, max_age=TASK_EXPIRATION_TIME) def check_email_configuration(): logger.info("Check for users without notification email") - count = 0 users = [] try: for u in User.all(): - if not u.email and len(UnconfiguredEmailNotification.find_by_user(u)) == 0: - users.append(u) - count += 1 + n_list = UnconfiguredEmailNotification.find_by_user(u) + if not u.email: + if len(n_list) == 0: + users.append(u) + elif len(n_list) > 0: + for n in n_list: + n.remove_user(u) + u.save() if len(users) > 0: n = UnconfiguredEmailNotification( "Unconfigured email", @@ -195,5 +199,4 @@ def check_email_configuration(): n.save() except Exception as e: logger.debug(e) - logger.error("Error when deleting notification %r", n) - logger.info("Check for users without notification email configured: generated notification for %r users", count) + logger.info("Check for users without notification email configured: generated a notification for users %r", users) From 5d792d1456ef30bffa11febd61f2e5fcef67f0a1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 10 Feb 2022 16:54:07 +0100 Subject: [PATCH 11/12] Fix missing property declaration --- lifemonitor/api/models/notifications.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lifemonitor/api/models/notifications.py b/lifemonitor/api/models/notifications.py index a7d23841b..db81c8894 100644 --- a/lifemonitor/api/models/notifications.py +++ b/lifemonitor/api/models/notifications.py @@ -42,6 +42,7 @@ class WorkflowStatusNotification(Notification): 'polymorphic_identity': 'workflow_status_notification' } + @property def get_icon_path(self) -> str: return 'lifemonitor/static/img/icons/' \ + ('times-circle-solid.svg' From a7cfb8c48925fb9501f678d0912b407390b8a0a0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 10 Feb 2022 16:54:35 +0100 Subject: [PATCH 12/12] pre-release v0.7.0 --- k8s/Chart.yaml | 4 ++-- lifemonitor/static/src/package.json | 2 +- specs/api.yaml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/k8s/Chart.yaml b/k8s/Chart.yaml index e0715ded5..a0675cb63 100644 --- a/k8s/Chart.yaml +++ b/k8s/Chart.yaml @@ -7,12 +7,12 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.6.0 +version: 0.7.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 0.6.0 +appVersion: 0.7.0 # Chart dependencies dependencies: diff --git a/lifemonitor/static/src/package.json b/lifemonitor/static/src/package.json index 6bed5ba02..2a94a2b69 100644 --- a/lifemonitor/static/src/package.json +++ b/lifemonitor/static/src/package.json @@ -1,7 +1,7 @@ { "name": "lifemonitor", "description": "Workflow Testing Service", - "version": "0.6.0", + "version": "0.7.0", "license": "MIT", "author": "CRS4", "main": "../dist/js/lifemonitor.min.js", diff --git a/specs/api.yaml b/specs/api.yaml index 0823cf431..d48f4b048 100644 --- a/specs/api.yaml +++ b/specs/api.yaml @@ -3,7 +3,7 @@ openapi: "3.0.0" info: - version: "0.6.0" + version: "0.7.0" title: "Life Monitor API" description: | *Workflow sustainability service* @@ -18,7 +18,7 @@ info: servers: - url: / description: > - Version 0.6.0 of API. + Version 0.7.0 of API. tags: - name: Registries