From 1bdb54df35847187efef291eac4f66f298f7ed61 Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Mon, 5 Jun 2023 11:49:39 +0800 Subject: [PATCH 1/9] Remove request reading middleware as we use post-buffering (#2094) # What this PR does RequestBodyReadingMiddleware is excess as [post-buffering is enabled](https://github.com/grafana/oncall/blob/dev/engine/uwsgi.ini#L17): If an HTTP request has a body (like a POST request generated by a form), you have to read (consume) it in your application. If you do not do this, the communication socket with your webserver may be clobbered. If you are lazy you can use the post-buffering option that will automatically read data for you. For Rack applications this is automatically enabled. (https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) ## Which issue(s) this PR fixes ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --- engine/engine/middlewares.py | 16 +--------------- engine/settings/base.py | 1 - 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/engine/engine/middlewares.py b/engine/engine/middlewares.py index 83ca2bf6..5af7fc31 100644 --- a/engine/engine/middlewares.py +++ b/engine/engine/middlewares.py @@ -3,9 +3,8 @@ import logging from django.apps import apps from django.conf import settings -from django.core.exceptions import PermissionDenied, RequestDataTooBig +from django.core.exceptions import PermissionDenied from django.db import OperationalError -from django.http import HttpResponse from django.utils.deprecation import MiddlewareMixin logger = logging.getLogger(__name__) @@ -57,19 +56,6 @@ class RequestTimeLoggingMiddleware(MiddlewareMixin): return response -class RequestBodyReadingMiddleware(MiddlewareMixin): - def process_request(self, request): - # Reading request body, as required by uwsgi - # https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html - # "If an HTTP request has a body (like a POST request generated by a form), - # you have to read (consume) it in your application. - # If you do not do this, the communication socket with your webserver may be clobbered." - try: - request.body - except RequestDataTooBig: - return HttpResponse(status=400) - - class BanAlertConsumptionBasedOnSettingsMiddleware(MiddlewareMixin): """ Banning requests for /integrations/v1 diff --git a/engine/settings/base.py b/engine/settings/base.py index ce6ec554..25056d8d 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -245,7 +245,6 @@ MIDDLEWARE = [ "log_request_id.middleware.RequestIDMiddleware", "engine.middlewares.RequestTimeLoggingMiddleware", "engine.middlewares.BanAlertConsumptionBasedOnSettingsMiddleware", - "engine.middlewares.RequestBodyReadingMiddleware", "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "debug_toolbar.middleware.DebugToolbarMiddleware", From d8e42c731deab01daccd0af4196e39b91747c662 Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Mon, 5 Jun 2023 16:43:10 +0800 Subject: [PATCH 2/9] Add 413 for requests with content-length > 15Mb on uwsgi (#2095) # What this PR does ## Which issue(s) this PR fixes ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --- engine/Dockerfile | 4 +++- engine/uwsgi.ini | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/engine/Dockerfile b/engine/Dockerfile index ad1bbd6c..0b8c98ad 100644 --- a/engine/Dockerfile +++ b/engine/Dockerfile @@ -7,7 +7,9 @@ RUN apt-get update && apt-get install -y \ netcat \ curl \ bash \ - git + git \ + libpcre3 \ + libpcre3-dev WORKDIR /etc/app COPY ./requirements.txt ./ diff --git a/engine/uwsgi.ini b/engine/uwsgi.ini index dbc489f1..2425c79a 100644 --- a/engine/uwsgi.ini +++ b/engine/uwsgi.ini @@ -17,6 +17,9 @@ http-timeout=620 post-buffering=1 enable-threads=true +; drop requests with CONTENT_LENGTH bigger than 15MB +route-if=ishigher:${CONTENT_LENGTH};15000000 break:413 Request Entity Too Large + logger=stdio log-format=source=engine:uwsgi status=%(status) method=%(method) path=%(uri) latency=%(secs) google_trace_id=%(var.HTTP_X_CLOUD_TRACE_CONTEXT) protocol=%(proto) resp_size=%(size) req_body_size=%(cl) log-encoder=format ${strftime:%%Y-%%m-%%d %%H:%%M:%%S} ${msgnl} From d3921a538603060d41dcf1e18bc7b50c6227b983 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Mon, 5 Jun 2023 10:03:38 +0100 Subject: [PATCH 3/9] Remove absolute aliases as it can cause unexpected redirects in future versions (#1793) If a page is removed in a future version, the presence of a "latest" absolute alias in a previous version can redirect that removed page. **Notes for reviewer:** I would like to make similar changes for older, previously published versions. I believe you are only publishing documentation on specific tag events and are not maintaining long lived release branches for backporting fixes into. If my understanding is correct, I will make the changes in the website repository (https://github.com/grafana/website/tree/master/content/docs/oncall). Signed-off-by: Jack Baldry --- docs/sources/_index.md | 2 -- docs/sources/escalation-chains-and-routes/_index.md | 2 -- docs/sources/get-started/_index.md | 1 - docs/sources/integrations/_index.md | 2 -- docs/sources/integrations/alertmanager/index.md | 1 - docs/sources/integrations/grafana-alerting/index.md | 1 - docs/sources/integrations/inbound-email/index.md | 1 - docs/sources/integrations/webhook/index.md | 1 - docs/sources/integrations/zabbix/index.md | 1 - docs/sources/jinja2-templating/_index.md | 2 -- docs/sources/mobile-app/_index.md | 2 -- docs/sources/notify/ms-teams/index.md | 1 - docs/sources/notify/slack/index.md | 1 - docs/sources/notify/telegram/index.md | 1 - docs/sources/on-call-schedules/ical-schedules/index.md | 5 ++--- docs/sources/on-call-schedules/web-schedule/_index.md | 3 +-- docs/sources/oncall-api-reference/_index.md | 2 -- docs/sources/oncall-api-reference/alertgroups.md | 2 -- docs/sources/oncall-api-reference/alerts.md | 2 -- docs/sources/oncall-api-reference/escalation_chains.md | 2 -- docs/sources/oncall-api-reference/escalation_policies.md | 2 -- docs/sources/oncall-api-reference/integrations.md | 2 -- docs/sources/oncall-api-reference/on_call_shifts.md | 2 -- docs/sources/oncall-api-reference/outgoing_webhooks.md | 2 -- .../oncall-api-reference/personal_notification_rules.md | 2 -- docs/sources/oncall-api-reference/postmortem_messages.md | 2 -- docs/sources/oncall-api-reference/postmortems.md | 2 -- docs/sources/oncall-api-reference/routes.md | 2 -- docs/sources/oncall-api-reference/schedules.md | 2 -- docs/sources/oncall-api-reference/slack_channels.md | 2 -- docs/sources/oncall-api-reference/user_groups.md | 2 -- docs/sources/oncall-api-reference/users.md | 2 -- docs/sources/open-source/_index.md | 2 -- docs/sources/outgoing-webhooks/_index.md | 1 - docs/sources/user-and-team-management/_index.md | 2 -- 35 files changed, 3 insertions(+), 61 deletions(-) diff --git a/docs/sources/_index.md b/docs/sources/_index.md index bb0c2a60..8fa2c245 100644 --- a/docs/sources/_index.md +++ b/docs/sources/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/ canonical: https://grafana.com/docs/oncall/latest/ keywords: - Grafana Cloud diff --git a/docs/sources/escalation-chains-and-routes/_index.md b/docs/sources/escalation-chains-and-routes/_index.md index 5045fb28..61bff7c9 100644 --- a/docs/sources/escalation-chains-and-routes/_index.md +++ b/docs/sources/escalation-chains-and-routes/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/escalation-chains-and-routes/ canonical: https://grafana.com/docs/oncall/latest/escalation-chains-and-routes/ title: Escalation Chains and Routes weight: 600 diff --git a/docs/sources/get-started/_index.md b/docs/sources/get-started/_index.md index d7ba8562..f36267e5 100644 --- a/docs/sources/get-started/_index.md +++ b/docs/sources/get-started/_index.md @@ -1,6 +1,5 @@ --- aliases: - - /docs/oncall/latest/get-started/ - /getting-started/ canonical: https://grafana.com/docs/oncall/latest/get-started/ keywords: diff --git a/docs/sources/integrations/_index.md b/docs/sources/integrations/_index.md index da9dec7d..822df7e6 100644 --- a/docs/sources/integrations/_index.md +++ b/docs/sources/integrations/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/integration-with-alert-sources/ canonical: https://grafana.com/docs/oncall/latest/integration-with-alert-sources/ keywords: - Grafana Cloud diff --git a/docs/sources/integrations/alertmanager/index.md b/docs/sources/integrations/alertmanager/index.md index df0e6f5d..d4912972 100644 --- a/docs/sources/integrations/alertmanager/index.md +++ b/docs/sources/integrations/alertmanager/index.md @@ -1,7 +1,6 @@ --- aliases: - add-alertmanager/ - - /docs/oncall/latest/integrations/available-integrations/configure-alertmanager/ canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-alertmanager/ keywords: - Grafana Cloud diff --git a/docs/sources/integrations/grafana-alerting/index.md b/docs/sources/integrations/grafana-alerting/index.md index 776fed61..a5fe7679 100644 --- a/docs/sources/integrations/grafana-alerting/index.md +++ b/docs/sources/integrations/grafana-alerting/index.md @@ -1,7 +1,6 @@ --- aliases: - add-grafana-alerting/ - - /docs/oncall/latest/integrations/available-integrations/configure-grafana-alerting/ canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-grafana-alerting/ keywords: - Grafana Cloud diff --git a/docs/sources/integrations/inbound-email/index.md b/docs/sources/integrations/inbound-email/index.md index c96fce3c..6c4c6b31 100644 --- a/docs/sources/integrations/inbound-email/index.md +++ b/docs/sources/integrations/inbound-email/index.md @@ -1,7 +1,6 @@ --- aliases: - inbound-email/ - - /docs/oncall/latest/integrations/available-integrations/configure-inbound-email/ canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-inbound-email/ keywords: - Grafana Cloud diff --git a/docs/sources/integrations/webhook/index.md b/docs/sources/integrations/webhook/index.md index 3052e5cb..66c03496 100644 --- a/docs/sources/integrations/webhook/index.md +++ b/docs/sources/integrations/webhook/index.md @@ -1,7 +1,6 @@ --- aliases: - ../add-webhook-integration/ - - /docs/oncall/latest/integrations/available-integrations/configure-webhook/ canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-webhook/ keywords: - Grafana Cloud diff --git a/docs/sources/integrations/zabbix/index.md b/docs/sources/integrations/zabbix/index.md index a5359743..38714a68 100644 --- a/docs/sources/integrations/zabbix/index.md +++ b/docs/sources/integrations/zabbix/index.md @@ -1,7 +1,6 @@ --- aliases: - add-zabbix/ - - /docs/oncall/latest/integrations/available-integrations/configure-zabbix/ canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-zabbix/ keywords: - Grafana Cloud diff --git a/docs/sources/jinja2-templating/_index.md b/docs/sources/jinja2-templating/_index.md index 5664821f..7fcd876c 100644 --- a/docs/sources/jinja2-templating/_index.md +++ b/docs/sources/jinja2-templating/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/jinja2-templating/ canonical: https://grafana.com/docs/oncall/latest/jinja2-templating/ title: Jinja2 templating weight: 1000 diff --git a/docs/sources/mobile-app/_index.md b/docs/sources/mobile-app/_index.md index be482075..d6d3d7b2 100644 --- a/docs/sources/mobile-app/_index.md +++ b/docs/sources/mobile-app/_index.md @@ -1,7 +1,5 @@ --- title: Mobile App -aliases: - - /docs/oncall/latest/mobile-app/ canonical: https://grafana.com/docs/oncall/latest/mobile-app/ keywords: - Mobile App diff --git a/docs/sources/notify/ms-teams/index.md b/docs/sources/notify/ms-teams/index.md index 602ef4d4..94821845 100644 --- a/docs/sources/notify/ms-teams/index.md +++ b/docs/sources/notify/ms-teams/index.md @@ -1,7 +1,6 @@ --- aliases: - ../../chat-options/configure-teams/ - - /docs/oncall/latest/integrations/chatops-integrations/configure-teams/ canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-teams/ keywords: - Grafana Cloud diff --git a/docs/sources/notify/slack/index.md b/docs/sources/notify/slack/index.md index e3f10238..9ad7594a 100644 --- a/docs/sources/notify/slack/index.md +++ b/docs/sources/notify/slack/index.md @@ -1,7 +1,6 @@ --- aliases: - ../../chat-options/configure-slack/ - - /docs/oncall/latest/integrations/chatops-integrations/configure-slack/ canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-slack/ keywords: - Grafana Cloud diff --git a/docs/sources/notify/telegram/index.md b/docs/sources/notify/telegram/index.md index 17080b2c..5b8dbcd2 100644 --- a/docs/sources/notify/telegram/index.md +++ b/docs/sources/notify/telegram/index.md @@ -1,7 +1,6 @@ --- aliases: - ../../chat-options/configure-telegram/ - - /docs/oncall/latest/integrations/chatops-integrations/configure-telegram/ canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-telegram/ keywords: - Grafana Cloud diff --git a/docs/sources/on-call-schedules/ical-schedules/index.md b/docs/sources/on-call-schedules/ical-schedules/index.md index 79b9dd75..d802f888 100644 --- a/docs/sources/on-call-schedules/ical-schedules/index.md +++ b/docs/sources/on-call-schedules/ical-schedules/index.md @@ -1,14 +1,13 @@ --- title: iCal on-call schedules -aliases: - - /docs/oncall/latest/on-call-schedules/ical-schedules/ canonical: https://grafana.com/docs/oncall/latest/on-call-schedules/ical-schedules/ description: "Learn how to manage on-call schedules with iCal import" keywords: - Grafana - oncall - on-call - - calendar + - calendar +title: Import on-call schedules weight: 300 --- diff --git a/docs/sources/on-call-schedules/web-schedule/_index.md b/docs/sources/on-call-schedules/web-schedule/_index.md index 582e1579..e29d7cad 100644 --- a/docs/sources/on-call-schedules/web-schedule/_index.md +++ b/docs/sources/on-call-schedules/web-schedule/_index.md @@ -1,7 +1,5 @@ --- title: Web-based on-call schedules -aliases: - - /docs/oncall/latest/on-call-schedules/web-schedule/ canonical: https://grafana.com/docs/oncall/latest/on-call-schedules/web-schedule/ description: "Learn more about Grafana OnCalls built in schedule tool" keywords: @@ -9,6 +7,7 @@ keywords: - oncall - schedule - calendar +title: Web-based schedules weight: 100 --- diff --git a/docs/sources/oncall-api-reference/_index.md b/docs/sources/oncall-api-reference/_index.md index 12ae513a..e289f07b 100644 --- a/docs/sources/oncall-api-reference/_index.md +++ b/docs/sources/oncall-api-reference/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/ title: Grafana OnCall HTTP API reference weight: 1500 diff --git a/docs/sources/oncall-api-reference/alertgroups.md b/docs/sources/oncall-api-reference/alertgroups.md index 5f5addf8..f0d68157 100644 --- a/docs/sources/oncall-api-reference/alertgroups.md +++ b/docs/sources/oncall-api-reference/alertgroups.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/alertgroups/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/alertgroups/ title: Alert groups HTTP API weight: 400 diff --git a/docs/sources/oncall-api-reference/alerts.md b/docs/sources/oncall-api-reference/alerts.md index ab4b6a3a..cfc19114 100644 --- a/docs/sources/oncall-api-reference/alerts.md +++ b/docs/sources/oncall-api-reference/alerts.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/alerts/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/alerts/ title: Alerts HTTP API weight: 100 diff --git a/docs/sources/oncall-api-reference/escalation_chains.md b/docs/sources/oncall-api-reference/escalation_chains.md index f88b8d71..301deb2a 100644 --- a/docs/sources/oncall-api-reference/escalation_chains.md +++ b/docs/sources/oncall-api-reference/escalation_chains.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/escalation_chains/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation_chains/ title: Escalation Chains HTTP API weight: 200 diff --git a/docs/sources/oncall-api-reference/escalation_policies.md b/docs/sources/oncall-api-reference/escalation_policies.md index 0cca0501..1c538759 100644 --- a/docs/sources/oncall-api-reference/escalation_policies.md +++ b/docs/sources/oncall-api-reference/escalation_policies.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/escalation_policies/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation_policies/ title: Escalation Policies HTTP API weight: 300 diff --git a/docs/sources/oncall-api-reference/integrations.md b/docs/sources/oncall-api-reference/integrations.md index a4af73b8..00f4702c 100644 --- a/docs/sources/oncall-api-reference/integrations.md +++ b/docs/sources/oncall-api-reference/integrations.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/integrations/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/integrations/ title: Integrations HTTP API weight: 500 diff --git a/docs/sources/oncall-api-reference/on_call_shifts.md b/docs/sources/oncall-api-reference/on_call_shifts.md index ff70f9c2..74917bf0 100644 --- a/docs/sources/oncall-api-reference/on_call_shifts.md +++ b/docs/sources/oncall-api-reference/on_call_shifts.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/on_call_shifts/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/on_call_shifts/ title: OnCall shifts HTTP API weight: 600 diff --git a/docs/sources/oncall-api-reference/outgoing_webhooks.md b/docs/sources/oncall-api-reference/outgoing_webhooks.md index deb26150..20bd59f0 100644 --- a/docs/sources/oncall-api-reference/outgoing_webhooks.md +++ b/docs/sources/oncall-api-reference/outgoing_webhooks.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/outgoing_webhooks/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/outgoing_webhooks/ title: Outgoing webhooks HTTP API weight: 700 diff --git a/docs/sources/oncall-api-reference/personal_notification_rules.md b/docs/sources/oncall-api-reference/personal_notification_rules.md index cff10373..01fe45d0 100644 --- a/docs/sources/oncall-api-reference/personal_notification_rules.md +++ b/docs/sources/oncall-api-reference/personal_notification_rules.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/personal_notification_rules/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/personal_notification_rules/ title: Personal Notification Rules HTTP API weight: 800 diff --git a/docs/sources/oncall-api-reference/postmortem_messages.md b/docs/sources/oncall-api-reference/postmortem_messages.md index f3f5aff8..237b7bff 100644 --- a/docs/sources/oncall-api-reference/postmortem_messages.md +++ b/docs/sources/oncall-api-reference/postmortem_messages.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/postmortem_messages/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/postmortem_messages/ draft: true title: Postmortem Messages HTTP API diff --git a/docs/sources/oncall-api-reference/postmortems.md b/docs/sources/oncall-api-reference/postmortems.md index 91797f74..41ae758c 100644 --- a/docs/sources/oncall-api-reference/postmortems.md +++ b/docs/sources/oncall-api-reference/postmortems.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/postmortems/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/postmortems/ draft: true title: Postmortem HTTP API diff --git a/docs/sources/oncall-api-reference/routes.md b/docs/sources/oncall-api-reference/routes.md index 49dfc25a..4d8987a2 100644 --- a/docs/sources/oncall-api-reference/routes.md +++ b/docs/sources/oncall-api-reference/routes.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/routes/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/routes/ title: Routes HTTP API weight: 1100 diff --git a/docs/sources/oncall-api-reference/schedules.md b/docs/sources/oncall-api-reference/schedules.md index 4be429b7..df5c6c64 100644 --- a/docs/sources/oncall-api-reference/schedules.md +++ b/docs/sources/oncall-api-reference/schedules.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/schedules/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/schedules/ title: Schedule HTTP API weight: 1200 diff --git a/docs/sources/oncall-api-reference/slack_channels.md b/docs/sources/oncall-api-reference/slack_channels.md index f5516688..82e78152 100644 --- a/docs/sources/oncall-api-reference/slack_channels.md +++ b/docs/sources/oncall-api-reference/slack_channels.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/slack_channels/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/slack_channels/ title: Slack Channels HTTP API weight: 1300 diff --git a/docs/sources/oncall-api-reference/user_groups.md b/docs/sources/oncall-api-reference/user_groups.md index 2c5966a0..b7f6e328 100644 --- a/docs/sources/oncall-api-reference/user_groups.md +++ b/docs/sources/oncall-api-reference/user_groups.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/user_groups/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/user_groups/ title: OnCall User Groups HTTP API weight: 1400 diff --git a/docs/sources/oncall-api-reference/users.md b/docs/sources/oncall-api-reference/users.md index 4c520eeb..9d205fb0 100644 --- a/docs/sources/oncall-api-reference/users.md +++ b/docs/sources/oncall-api-reference/users.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/users/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/users/ title: Grafana OnCall Users HTTP API weight: 1500 diff --git a/docs/sources/open-source/_index.md b/docs/sources/open-source/_index.md index 1674e9be..c0c92316 100644 --- a/docs/sources/open-source/_index.md +++ b/docs/sources/open-source/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/open-source/ keywords: - Open Source title: Open Source diff --git a/docs/sources/outgoing-webhooks/_index.md b/docs/sources/outgoing-webhooks/_index.md index 7b80a8da..a871b624 100644 --- a/docs/sources/outgoing-webhooks/_index.md +++ b/docs/sources/outgoing-webhooks/_index.md @@ -1,7 +1,6 @@ --- aliases: - ../outgoing-webhooks/ - - /docs/oncall/latest/outgoing-webhooks/ canonical: https://grafana.com/docs/oncall/latest/outgoing-webhooks/ keywords: - Grafana Cloud diff --git a/docs/sources/user-and-team-management/_index.md b/docs/sources/user-and-team-management/_index.md index 07a78dc1..447f0b5c 100644 --- a/docs/sources/user-and-team-management/_index.md +++ b/docs/sources/user-and-team-management/_index.md @@ -1,7 +1,5 @@ --- title: User and team management -aliases: - - /docs/oncall/latest/user-and-team-management/ keywords: - oncall - RBAC From 5f067af14f16e41d1340b7d865745cb23eb2c6ac Mon Sep 17 00:00:00 2001 From: Alexander Cherepanov Date: Mon, 5 Jun 2023 18:25:51 +0600 Subject: [PATCH 4/9] Do not hide not secret settings in the web plugin UI (#1964) # What this PR does In the web plugin UI Settings -> Env Variables if variable is not secret, do not hide its value. ## Which issue(s) this PR fixes ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --------- Co-authored-by: Joey Orlando Co-authored-by: Joey Orlando Co-authored-by: Vadim Stepanov --- CHANGELOG.md | 6 ++++++ .../pages/settings/tabs/LiveSettings/LiveSettingsPage.tsx | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fc43310..5ada6af0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Do not hide not secret settings in the web plugin UI by @alexintech ([#1964](https://github.com/grafana/oncall/pull/1964)) + ## v1.2.36 (2023-06-02) ### Fixed diff --git a/grafana-plugin/src/pages/settings/tabs/LiveSettings/LiveSettingsPage.tsx b/grafana-plugin/src/pages/settings/tabs/LiveSettings/LiveSettingsPage.tsx index b93662e6..e6f6da03 100644 --- a/grafana-plugin/src/pages/settings/tabs/LiveSettings/LiveSettingsPage.tsx +++ b/grafana-plugin/src/pages/settings/tabs/LiveSettings/LiveSettingsPage.tsx @@ -166,7 +166,7 @@ class LiveSettings extends React.Component onTextChange={this.getEditValueChangeHandler(item)} editable={isUserActionAllowed(UserActions.OtherSettingsWrite)} clearBeforeEdit={item.is_secret} - hidden={hideValues} + hidden={hideValues && item.is_secret} > {normalizeValue(item.value)} @@ -208,7 +208,7 @@ class LiveSettings extends React.Component return (
- {hideValues ? PLACEHOLDER : normalizeValue(item.default_value)} + {hideValues && item.is_secret ? PLACEHOLDER : normalizeValue(item.default_value)}
); }; From 7ed6290d426ba90aa2e94843f0fee03a8832d949 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 5 Jun 2023 16:06:10 +0200 Subject: [PATCH 5/9] public API endpoint to export schedule final shifts (#2047) # What this PR does Closes https://github.com/grafana/oncall-private/issues/1632 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --- CHANGELOG.md | 4 + .../sources/oncall-api-reference/schedules.md | 168 ++++++++++++++++++ engine/apps/api/tests/test_oncall_shift.py | 12 +- engine/apps/api/tests/test_schedules.py | 16 +- .../public_api/serializers/schedules_base.py | 14 ++ .../apps/public_api/tests/test_schedules.py | 144 +++++++++++++++ engine/apps/public_api/views/schedules.py | 54 ++++++ .../apps/schedules/models/on_call_schedule.py | 11 ++ .../schedules/tests/test_on_call_schedule.py | 14 +- 9 files changed, 424 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ada6af0..66c764e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## v1.2.36 (2023-06-02) +### Added + +- Add public API endpoint to export a schedule's final shifts by @joeyorlando ([2047](https://github.com/grafana/oncall/pull/2047)) + ### Fixed - Fix demo alert for inbound email integration by @vadimkerr ([#2081](https://github.com/grafana/oncall/pull/2081)) diff --git a/docs/sources/oncall-api-reference/schedules.md b/docs/sources/oncall-api-reference/schedules.md index df5c6c64..9f9410a9 100644 --- a/docs/sources/oncall-api-reference/schedules.md +++ b/docs/sources/oncall-api-reference/schedules.md @@ -195,3 +195,171 @@ curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/" \ **HTTP request** `DELETE {{API_URL}}/api/v1/schedules//` + +# Export a schedule's final shifts + +**HTTP request** + +```shell +curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/final_shifts?start_date=2023-01-01&end_date=2023-02-01" \ + --request GET \ + --header "Authorization: meowmeowmeow" +``` + +The above command returns JSON structured in the following way: + +```json +{ + "count": 12, + "next": null, + "previous": null, + "results": [ + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-02T09:00:00Z", + "shift_end": "2023-01-02T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-04T09:00:00Z", + "shift_end": "2023-01-04T17:00:00Z" + }, + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-06T09:00:00Z", + "shift_end": "2023-01-06T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-09T09:00:00Z", + "shift_end": "2023-01-09T17:00:00Z" + }, + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-11T09:00:00Z", + "shift_end": "2023-01-11T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-13T09:00:00Z", + "shift_end": "2023-01-13T17:00:00Z" + }, + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-16T09:00:00Z", + "shift_end": "2023-01-16T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-18T09:00:00Z", + "shift_end": "2023-01-18T17:00:00Z" + }, + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-20T09:00:00Z", + "shift_end": "2023-01-20T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-23T09:00:00Z", + "shift_end": "2023-01-23T17:00:00Z" + }, + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-25T09:00:00Z", + "shift_end": "2023-01-25T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-27T09:00:00Z", + "shift_end": "2023-01-27T17:00:00Z" + } + ] +} +``` + +## Caveats + +Some notes on the `start_date` and `end_date` query parameters: + +- they are both required and should represent ISO 8601 formatted dates +- `end_date` must be greater than or equal to `start_date` +- `end_date` cannot be more than 365 days in the future from `start_date` + +Lastly, this endpoint is currently only active for web schedules. It will return HTTP 400 for schedules +defined via Terraform or iCal. + +## Example script to transform data to .csv for all of your schedules + +The following Python script will generate a `.csv` file, `oncall-report-2023-01-01-to-2023-01-31.csv`. This file will +contain three columns, `user_pk`, `user_email`, and `hours_on_call`, which represents how many hours each user was +on call during the period starting January 1, 2023 to January 31, 2023 (inclusive). + +```python +import csv +import requests +from datetime import datetime + +# CUSTOMIZE THE FOLLOWING VARIABLES +START_DATE = "2023-01-01" +END_DATE = "2023-01-31" +OUTPUT_FILE_NAME = f"oncall-report-{START_DATE}-to-{END_DATE}.csv" +MY_ONCALL_API_BASE_URL = "https://oncall-prod-us-central-0.grafana.net/oncall/api/v1/schedules" +MY_ONCALL_API_KEY = "meowmeowwoofwoof" + +headers = {"Authorization": MY_ONCALL_API_KEY} +schedule_ids = [schedule["id"] for schedule in requests.get(MY_ONCALL_API_BASE_URL, headers=headers).json()["results"]] +user_on_call_hours = {} + +for schedule_id in schedule_ids: + response = requests.get( + f"{MY_ONCALL_API_BASE_URL}/{schedule_id}/final_shifts?start_date={START_DATE}&end_date={END_DATE}", + headers=headers) + + for final_shift in response.json()["results"]: + user_pk = final_shift["user_pk"] + end = datetime.fromisoformat(final_shift["shift_end"]) + start = datetime.fromisoformat(final_shift["shift_start"]) + shift_time_in_seconds = (end - start).total_seconds() + shift_time_in_hours = shift_time_in_seconds / (60 * 60) + + if user_pk in user_on_call_hours: + user_on_call_hours[user_pk]["hours_on_call"] += shift_time_in_hours + else: + user_on_call_hours[user_pk] = { + "email": final_shift["user_email"], + "hours_on_call": shift_time_in_hours, + } + +with open(OUTPUT_FILE_NAME, "w") as fp: + csv_writer = csv.DictWriter(fp, ["user_pk", "user_email", "hours_on_call"]) + csv_writer.writeheader() + + for user_pk, user_info in user_on_call_hours.items(): + csv_writer.writerow({ + "user_pk": user_pk, "user_email": user_info["email"], "hours_on_call": user_info["hours_on_call"]}) +``` diff --git a/engine/apps/api/tests/test_oncall_shift.py b/engine/apps/api/tests/test_oncall_shift.py index d92f4c4f..d7fd687d 100644 --- a/engine/apps/api/tests/test_oncall_shift.py +++ b/engine/apps/api/tests/test_oncall_shift.py @@ -1363,7 +1363,9 @@ def test_on_call_shift_preview( "is_gap": False, "priority_level": 2, "missing_users": [], - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + {"display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email} + ], "source": "web", } ] @@ -1653,7 +1655,9 @@ def test_on_call_shift_preview_update( "is_gap": False, "priority_level": 1, "missing_users": [], - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + {"display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email} + ], "source": "web", } assert rotation_events[-1] == expected_shift_preview @@ -1764,7 +1768,9 @@ def test_on_call_shift_preview_update_not_started_reuse_pk( "is_gap": False, "priority_level": 1, "missing_users": [], - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + {"display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email} + ], "source": "web", }, ] diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index 7a00c7ef..5a574894 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -812,7 +812,7 @@ def test_events_calendar( "all_day": False, "start": on_call_shift.start, "end": on_call_shift.start + on_call_shift.duration, - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", @@ -878,7 +878,7 @@ def test_filter_events_calendar( "all_day": False, "start": mon_start, "end": mon_start + on_call_shift.duration, - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", @@ -894,7 +894,7 @@ def test_filter_events_calendar( "all_day": False, "start": fri_start, "end": fri_start + on_call_shift.duration, - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", @@ -977,7 +977,7 @@ def test_filter_events_range_calendar( "all_day": False, "start": fri_start, "end": fri_start + on_call_shift.duration, - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", @@ -1059,7 +1059,13 @@ def test_filter_events_overrides( "all_day": False, "start": override_start, "end": override_start + override.duration, - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + { + "display_name": other_user.username, + "pk": other_user.public_primary_key, + "email": other_user.email, + } + ], "missing_users": [], "priority_level": None, "source": "api", diff --git a/engine/apps/public_api/serializers/schedules_base.py b/engine/apps/public_api/serializers/schedules_base.py index d69c620c..42742cb4 100644 --- a/engine/apps/public_api/serializers/schedules_base.py +++ b/engine/apps/public_api/serializers/schedules_base.py @@ -71,3 +71,17 @@ class ScheduleBaseSerializer(serializers.ModelSerializer): } return result + + +class FinalShiftQueryParamsSerializer(serializers.Serializer): + start_date = serializers.DateField(required=True) + end_date = serializers.DateField(required=True) + + def validate(self, attrs): + if attrs["start_date"] > attrs["end_date"]: + raise serializers.ValidationError("start_date must be less than or equal to end_date") + if attrs["end_date"] - attrs["start_date"] > timezone.timedelta(days=365): + raise serializers.ValidationError( + "The difference between start_date and end_date must be less than one year (365 days)" + ) + return attrs diff --git a/engine/apps/public_api/tests/test_schedules.py b/engine/apps/public_api/tests/test_schedules.py index b4a40279..1e5a2ba5 100644 --- a/engine/apps/public_api/tests/test_schedules.py +++ b/engine/apps/public_api/tests/test_schedules.py @@ -1,3 +1,4 @@ +import collections from unittest.mock import patch import pytest @@ -781,3 +782,146 @@ def test_create_ical_schedule_without_ical_url(make_organization_and_user_with_t } response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_oncall_shifts_request_validation( + make_organization_and_user_with_token, + make_schedule, +): + organization, _, token = make_organization_and_user_with_token() + ical_schedule = make_schedule(organization, schedule_class=OnCallScheduleICal) + terraform_schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) + web_schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) + + schedule_type_validation_msg = "OnCall shifts exports are currently only available for web calendars" + valid_date_msg = "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." + + client = APIClient() + + def _make_request(schedule, query_params=""): + url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key}) + return client.get(f"{url}{query_params}", format="json", HTTP_AUTHORIZATION=token) + + # only web schedules are allowed for now + response = _make_request(ical_schedule) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == schedule_type_validation_msg + + response = _make_request(terraform_schedule) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == schedule_type_validation_msg + + # query param validation + response = _make_request(web_schedule, "?start_date=2021-01-01") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["end_date"][0] == "This field is required." + + response = _make_request(web_schedule, "?start_date=asdfasdf") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["start_date"][0] == valid_date_msg + + response = _make_request(web_schedule, "?end_date=2021-01-01") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["start_date"][0] == "This field is required." + + response = _make_request(web_schedule, "?start_date=2021-01-01&end_date=asdfasdf") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["end_date"][0] == valid_date_msg + + response = _make_request(web_schedule, "?end_date=2021-01-01&start_date=2022-01-01") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == { + "non_field_errors": [ + "start_date must be less than or equal to end_date", + ] + } + + response = _make_request(web_schedule, "?end_date=2021-01-01&start_date=2019-12-31") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == { + "non_field_errors": [ + "The difference between start_date and end_date must be less than one year (365 days)", + ] + } + + +@pytest.mark.django_db +def test_oncall_shifts_export( + make_organization_and_user_with_token, + make_user, + make_schedule, + make_on_call_shift, +): + organization, _, token = make_organization_and_user_with_token() + + user1_email = "alice909450945045@example.com" + user2_email = "bob123123123123123@example.com" + user1_username = "alice" + user2_username = "bob" + + user1 = make_user(organization=organization, email=user1_email, username=user1_username) + user2 = make_user(organization=organization, email=user2_email, username=user2_username) + + user1_public_primary_key = user1.public_primary_key + user2_public_primary_key = user2.public_primary_key + schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) + + start_date = timezone.datetime(2023, 1, 1, 9, 0, 0) + make_on_call_shift( + organization=organization, + schedule=schedule, + shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, + frequency=CustomOnCallShift.FREQUENCY_DAILY, + priority_level=1, + interval=1, + by_day=["MO", "WE", "FR"], + start=start_date, + until=start_date + timezone.timedelta(days=28), + rolling_users=[{user1.pk: user1_public_primary_key}, {user2.pk: user2_public_primary_key}], + rotation_start=start_date, + duration=timezone.timedelta(hours=8), + ) + + client = APIClient() + + url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key}) + response = client.get(f"{url}?start_date=2023-01-01&end_date=2023-02-01", format="json", HTTP_AUTHORIZATION=token) + response_json = response.json() + shifts = response_json["results"] + + total_time_on_call = collections.defaultdict(int) + pk_to_user_mapping = { + user1_public_primary_key: { + "email": user1_email, + "username": user1_username, + }, + user2_public_primary_key: { + "email": user2_email, + "username": user2_username, + }, + } + + for row in shifts: + user_pk = row["user_pk"] + + # make sure we're exporting email and username as well + assert pk_to_user_mapping[user_pk]["email"] == row["user_email"] + assert pk_to_user_mapping[user_pk]["username"] == row["user_username"] + + end = timezone.datetime.fromisoformat(row["shift_end"]) + start = timezone.datetime.fromisoformat(row["shift_start"]) + shift_time_in_seconds = (end - start).total_seconds() + total_time_on_call[row["user_pk"]] += shift_time_in_seconds / (60 * 60) + + assert response.status_code == status.HTTP_200_OK + + # 3 shifts per week x 4 weeks x 8 hours per shift = 96 / 2 users = 48h per user for this period + expected_time_on_call = 48 + assert total_time_on_call[user1_public_primary_key] == expected_time_on_call + assert total_time_on_call[user2_public_primary_key] == expected_time_on_call + + # pagination parameters are mocked out for now + assert response_json["next"] is None + assert response_json["previous"] is None + assert response_json["count"] == len(shifts) diff --git a/engine/apps/public_api/views/schedules.py b/engine/apps/public_api/views/schedules.py index 39d5d26d..ab3aec8e 100644 --- a/engine/apps/public_api/views/schedules.py +++ b/engine/apps/public_api/views/schedules.py @@ -1,3 +1,5 @@ +import logging + from django_filters import rest_framework as filters from rest_framework import status from rest_framework.decorators import action @@ -9,9 +11,11 @@ from rest_framework.viewsets import ModelViewSet from apps.auth_token.auth import ApiTokenAuthentication, ScheduleExportAuthentication from apps.public_api.custom_renderers import CalendarRenderer from apps.public_api.serializers import PolymorphicScheduleSerializer, PolymorphicScheduleUpdateSerializer +from apps.public_api.serializers.schedules_base import FinalShiftQueryParamsSerializer from apps.public_api.throttlers.user_throttle import UserThrottle from apps.schedules.ical_utils import ical_export_from_schedule from apps.schedules.models import OnCallSchedule, OnCallScheduleWeb +from apps.schedules.models.on_call_schedule import ScheduleEvents, ScheduleFinalShifts from apps.slack.tasks import update_slack_user_group_for_schedules from common.api_helpers.exceptions import BadRequest from common.api_helpers.filters import ByTeamFilter @@ -19,6 +23,8 @@ from common.api_helpers.mixins import RateLimitHeadersMixin, UpdateSerializerMix from common.api_helpers.paginators import FiftyPageSizePaginator from common.insight_log import EntityEvent, write_resource_insight_log +logger = logging.getLogger(__name__) + class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet): authentication_classes = (ApiTokenAuthentication,) @@ -120,3 +126,51 @@ class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, Mo # Not using existing get_object method because it requires access to the organization user attribute export = ical_export_from_schedule(self.request.auth.schedule) return Response(export, status=status.HTTP_200_OK) + + @action(methods=["get"], detail=True) + def final_shifts(self, request, pk): + schedule = self.get_object() + + if not isinstance(schedule, OnCallScheduleWeb): + return Response( + "OnCall shifts exports are currently only available for web calendars", + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = FinalShiftQueryParamsSerializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + + start_date = serializer.validated_data["start_date"] + end_date = serializer.validated_data["end_date"] + days_between_start_and_end = (end_date - start_date).days + + final_schedule_events: ScheduleEvents = schedule.final_events("UTC", start_date, days_between_start_and_end) + + logger.info( + f"Exporting oncall shifts for schedule {pk} between dates {start_date} and {end_date}. {len(final_schedule_events)} shift events were found." + ) + + data: ScheduleFinalShifts = [ + { + "user_pk": user["pk"], + "user_email": user["email"], + "user_username": user["display_name"], + "shift_start": event["start"], + "shift_end": event["end"], + } + for event in final_schedule_events + for user in event["users"] + ] + + # right now we'll "mock out" the pagination related parameters (next and previous) + # rather than use a Pagination class from drf (as currently it operates on querysets). We've decided on this + # to make this response schema consistent with the rest of the public API + make it easy to add pagination + # here in the future (should we decide to migrate "final_shifts" to an actual model) + return Response( + { + "count": len(data), + "next": None, + "previous": None, + "results": data, + } + ) diff --git a/engine/apps/schedules/models/on_call_schedule.py b/engine/apps/schedules/models/on_call_schedule.py index 816784c8..0f1f99de 100644 --- a/engine/apps/schedules/models/on_call_schedule.py +++ b/engine/apps/schedules/models/on_call_schedule.py @@ -76,6 +76,7 @@ class QualityReport(TypedDict): class ScheduleEventUser(TypedDict): display_name: str pk: str + email: str class ScheduleEventShift(TypedDict): @@ -97,8 +98,17 @@ class ScheduleEvent(TypedDict): shift: ScheduleEventShift +class ScheduleFinalShift(TypedDict): + user_pk: str + user_email: str + user_username: str + shift_start: str + shift_end: str + + ScheduleEvents = List[ScheduleEvent] ScheduleEventIntervals = List[List[datetime.datetime]] +ScheduleFinalShifts = List[ScheduleFinalShift] def generate_public_primary_key_for_oncall_schedule_channel(): @@ -323,6 +333,7 @@ class OnCallSchedule(PolymorphicModel): "users": [ { "display_name": user.username, + "email": user.email, "pk": user.public_primary_key, } for user in shift["users"] diff --git a/engine/apps/schedules/tests/test_on_call_schedule.py b/engine/apps/schedules/tests/test_on_call_schedule.py index 3e7e7121..a61c7432 100644 --- a/engine/apps/schedules/tests/test_on_call_schedule.py +++ b/engine/apps/schedules/tests/test_on_call_schedule.py @@ -93,7 +93,7 @@ def test_filter_events(make_organization, make_user_for_organization, make_sched "is_gap": False, "priority_level": on_call_shift.priority_level, "missing_users": [], - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "shift": {"pk": on_call_shift.public_primary_key}, "source": "api", } @@ -114,7 +114,7 @@ def test_filter_events(make_organization, make_user_for_organization, make_sched "is_gap": False, "priority_level": None, "missing_users": [], - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "shift": {"pk": override.public_primary_key}, "source": "api", } @@ -179,7 +179,7 @@ def test_filter_events_include_gaps(make_organization, make_user_for_organizatio "is_gap": False, "priority_level": on_call_shift.priority_level, "missing_users": [], - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "shift": {"pk": on_call_shift.public_primary_key}, "source": "api", }, @@ -688,7 +688,9 @@ def test_preview_shift(make_organization, make_user_for_organization, make_sched "is_gap": False, "priority_level": new_shift.priority_level, "missing_users": [], - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + {"display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email} + ], "shift": {"pk": new_shift.public_primary_key}, "source": "api", } @@ -846,7 +848,9 @@ def test_preview_override_shift(make_organization, make_user_for_organization, m "is_gap": False, "priority_level": None, "missing_users": [], - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + {"display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email} + ], "shift": {"pk": new_shift.public_primary_key}, "source": "api", } From 021cf095a2d86a868d1882be73d76181a054a3e5 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Mon, 5 Jun 2023 14:24:59 -0300 Subject: [PATCH 6/9] Add support to update web schedule rotations in-place (#2102) --- engine/apps/api/serializers/on_call_shifts.py | 3 +- engine/apps/api/tests/test_oncall_shift.py | 50 +++++++++++++++++++ engine/apps/api/views/on_call_shifts.py | 3 +- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/engine/apps/api/serializers/on_call_shifts.py b/engine/apps/api/serializers/on_call_shifts.py index c6e66a09..f22baddb 100644 --- a/engine/apps/api/serializers/on_call_shifts.py +++ b/engine/apps/api/serializers/on_call_shifts.py @@ -195,6 +195,7 @@ class OnCallShiftUpdateSerializer(OnCallShiftSerializer): validated_data = self._correct_validated_data(instance.type, validated_data) change_only_title = True create_or_update_last_shift = False + force_update = validated_data.pop("force_update", True) for field in validated_data: if field != "title" and validated_data[field] != getattr(instance, field): @@ -209,7 +210,7 @@ class OnCallShiftUpdateSerializer(OnCallShiftSerializer): elif instance.event_is_finished: raise serializers.ValidationError(["This event cannot be updated"]) - if create_or_update_last_shift: + if not force_update and create_or_update_last_shift: result = instance.create_or_update_last_shift(validated_data) else: result = super().update(instance, validated_data) diff --git a/engine/apps/api/tests/test_oncall_shift.py b/engine/apps/api/tests/test_oncall_shift.py index d7fd687d..94c1eba2 100644 --- a/engine/apps/api/tests/test_oncall_shift.py +++ b/engine/apps/api/tests/test_oncall_shift.py @@ -402,6 +402,56 @@ def test_update_started_on_call_shift( assert on_call_shift.until == on_call_shift.updated_shift.rotation_start +@pytest.mark.django_db +def test_update_started_on_call_shift_force_update( + on_call_shift_internal_api_setup, + make_on_call_shift, + make_user_auth_headers, +): + token, user1, _, _, schedule = on_call_shift_internal_api_setup + + client = APIClient() + start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0) + + title = "Test Shift Rotation" + on_call_shift = make_on_call_shift( + schedule.organization, + shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, + schedule=schedule, + title=title, + start=start_date, + duration=timezone.timedelta(hours=3), + rotation_start=start_date, + rolling_users=[{user1.pk: user1.public_primary_key}], + ) + data_to_update = { + "title": title, + "priority_level": 2, + "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), + "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), + "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), + "until": None, + "frequency": None, + "interval": None, + "by_day": None, + "rolling_users": [[user1.public_primary_key]], + } + + assert on_call_shift.priority_level != data_to_update["priority_level"] + + url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) + "?force=true" + + response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) + + assert response.status_code == status.HTTP_200_OK + # check no shift was created + assert response.data["id"] == on_call_shift.public_primary_key + on_call_shift.refresh_from_db() + assert on_call_shift.priority_level == data_to_update["priority_level"] + assert on_call_shift.updated_shift is None + assert on_call_shift.until is None + + @pytest.mark.django_db def test_update_old_on_call_shift_with_future_version( on_call_shift_internal_api_setup, diff --git a/engine/apps/api/views/on_call_shifts.py b/engine/apps/api/views/on_call_shifts.py index b24482c1..c760b53f 100644 --- a/engine/apps/api/views/on_call_shifts.py +++ b/engine/apps/api/views/on_call_shifts.py @@ -71,7 +71,8 @@ class OnCallShiftView(TeamFilteringMixin, PublicPrimaryKeyMixin, UpdateSerialize def perform_update(self, serializer): prev_state = serializer.instance.insight_logs_serialized - serializer.save() + force_update = self.request.query_params.get("force", "") == "true" + serializer.save(force_update=force_update) new_state = serializer.instance.insight_logs_serialized write_resource_insight_log( instance=serializer.instance, From d9a1a3370348b81e8f24cbefca8b25bf033cdd00 Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Tue, 6 Jun 2023 09:45:23 +0800 Subject: [PATCH 7/9] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66c764e6..ff441f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## v1.2.37 (2023-06-06) + ### Changed - Do not hide not secret settings in the web plugin UI by @alexintech ([#1964](https://github.com/grafana/oncall/pull/1964)) From df0aedc8b49181a08fcb77a33c72fa3e4c7bae04 Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Tue, 6 Jun 2023 09:46:41 +0800 Subject: [PATCH 8/9] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff441f20..84e5ef70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -## v1.2.37 (2023-06-06) +## v1.2.38 (2023-06-06) ### Changed From 889c0afab9bd873084d6e5b1b30a8b01493a0725 Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Tue, 6 Jun 2023 10:11:11 +0800 Subject: [PATCH 9/9] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84e5ef70..56583ebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -## v1.2.38 (2023-06-06) +## v1.2.39 (2023-06-06) ### Changed