This commit is contained in:
Joey Orlando 2023-06-29 13:05:00 +02:00 committed by GitHub
commit 228889f850
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 109 additions and 164 deletions

View file

@ -144,7 +144,7 @@ steps:
# Services for Unit Test Backend
services:
- name: rabbit_test
image: rabbitmq:3.7.19
image: rabbitmq:3.12.0
environment:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
@ -224,10 +224,8 @@ trigger:
---
kind: pipeline
type: docker
name: OSS engine release (amd64)
platform:
os: linux
arch: amd64
name: OSS engine release
steps:
- name: set engine version
image: alpine
@ -244,7 +242,8 @@ steps:
DOCKER_BUILDKIT: 1
settings:
repo: grafana/oncall
tags: ${DRONE_TAG}-amd64-linux
tags: ${DRONE_TAG}
platform: linux/arm64/v8,linux/amd64
dockerfile: engine/Dockerfile
target: prod
context: engine/
@ -263,90 +262,6 @@ trigger:
ref:
- refs/tags/v*.*.*
---
kind: pipeline
type: docker
name: OSS engine release (arm64)
platform:
os: linux
arch: arm64
steps:
- name: set engine version
image: alpine
commands:
- apk add --no-cache bash sed
- if [ -z "$DRONE_TAG" ]; then echo "No tag, not modifying version"; else sed "0,/VERSION.*/ s/VERSION.*/VERSION = \"${DRONE_TAG}\"/g" engine/settings/base.py > engine/settings/base.temp && mv engine/settings/base.temp engine/settings/base.py; fi
- cat engine/settings/base.py | grep VERSION | head -1
- name: build and push docker image
image: plugins/docker
environment:
# force docker to use buildkit feature, this will skip build stages that aren't required in the final image (ie. dev & dev-enterprise)
# https://github.com/docker/cli/issues/1134#issuecomment-406449342
DOCKER_BUILDKIT: 1
settings:
repo: grafana/oncall
tags: ${DRONE_TAG}-arm64-linux
dockerfile: engine/Dockerfile
target: prod
context: engine/
password:
from_secret: docker_password
username:
from_secret: docker_username
depends_on:
- set engine version
trigger:
event:
- promote
target:
- oss
ref:
- refs/tags/v*.*.*
---
depends_on:
- OSS engine release (amd64)
- OSS engine release (arm64)
kind: pipeline
type: docker
name: manifest
steps:
- name: manifest tag
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
target: "grafana/oncall:${DRONE_TAG}"
template: "grafana/oncall:${DRONE_TAG}-ARCH-OS"
platforms:
- linux/amd64
- linux/arm64
- name: manifest latest
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
target: "grafana/oncall:latest"
template: "grafana/oncall:${DRONE_TAG}-ARCH-OS"
platforms:
- linux/amd64
- linux/arm64
trigger:
event:
- promote
target:
- oss
ref:
- refs/tags/v*.*.*
---
# Secret for pulling docker images.
kind: secret
@ -418,6 +333,6 @@ kind: secret
name: drone_token
---
kind: signature
hmac: 59ad498428731001484be9ac6b9d10685b2cd49f30eb3d97ed7dc99f031dca22
hmac: 610fb051f4361baa2621f6aa41c18b3afee5160492b30f5a5f23fcecf86f1b0c
...

View file

@ -96,7 +96,7 @@ jobs:
SLACK_CLIENT_OAUTH_ID: 1
services:
rabbit_test:
image: rabbitmq:3.7.19
image: rabbitmq:3.12.0
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
@ -148,7 +148,7 @@ jobs:
ONCALL_TESTING_RBAC_ENABLED: ${{ matrix.rbac_enabled }}
services:
rabbit_test:
image: rabbitmq:3.7.19
image: rabbitmq:3.12.0
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
@ -192,7 +192,7 @@ jobs:
ONCALL_TESTING_RBAC_ENABLED: ${{ matrix.rbac_enabled }}
services:
rabbit_test:
image: rabbitmq:3.7.19
image: rabbitmq:3.12.0
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq

View file

@ -7,11 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v1.3.3
## v1.3.4 (2023-06-29)
This version contains just some small cleanup and CI changes 🙂
## v1.3.3 (2023-06-28)
### Added
- Add metric "how many alert groups user was notified of" to Prometheus exporter ([#2334](https://github.com/grafana/oncall/pull/2334/))
## v1.3.2
## v1.3.2 (2023-06-28)
### Changed

View file

@ -179,7 +179,7 @@ services:
rabbitmq:
container_name: rabbitmq
labels: *oncall-labels
image: "rabbitmq:3.7.15-management"
image: "rabbitmq:3.12.0-management"
restart: always
environment:
RABBITMQ_DEFAULT_USER: "rabbitmq"

View file

@ -99,7 +99,7 @@ services:
cpus: "0.1"
rabbitmq:
image: "rabbitmq:3.7.15-management"
image: "rabbitmq:3.12.0-management"
restart: always
hostname: rabbitmq
volumes:

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.19 on 2023-06-28 16:00
from django.db import migrations
import django_migration_linter as linter
class Migration(migrations.Migration):
dependencies = [
('alerts', '0017_alertgroup_is_restricted'),
]
operations = [
linter.IgnoreMigration(), # This field is deprecated a long time ago.
migrations.RemoveField(
model_name='alertreceivechannel',
name='integration_slack_channel_id',
),
]

View file

@ -160,8 +160,6 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
verbal_name = models.CharField(max_length=150, null=True, default=None)
description_short = models.CharField(max_length=250, null=True, default=None)
integration_slack_channel_id = models.CharField(max_length=150, null=True, default=None)
is_finished_alerting_setup = models.BooleanField(default=False)
# *_*_template fields are legacy way of storing templates

View file

@ -14,7 +14,7 @@ def test_render_for_phone_call(
make_alert,
):
organization, _ = make_organization_with_slack_team_identity()
alert_receive_channel = make_alert_receive_channel(organization, integration_slack_channel_id="CWER1ASD")
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
SlackMessage.objects.create(channel_id="CWER1ASD", alert_group=alert_group)
@ -60,7 +60,7 @@ def test_delete(
slack_channel = make_slack_channel(slack_team_identity, name="general", slack_id="CWER1ASD")
user = make_user(organization=organization)
alert_receive_channel = make_alert_receive_channel(organization, integration_slack_channel_id="CWER1ASD")
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
SlackMessage.objects.create(channel_id="CWER1ASD", alert_group=alert_group)
@ -104,7 +104,7 @@ def test_alerts_count_gt(
make_alert,
):
organization = make_organization()
alert_receive_channel = make_alert_receive_channel(organization, integration_slack_channel_id="CWER1ASD")
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)

View file

@ -69,9 +69,9 @@ class OrderedModel(models.Model):
def save(self, *args, **kwargs) -> None:
if self.order is None:
self._save_no_order_provided()
self._save_no_order_provided(*args, **kwargs)
else:
super().save()
super().save(*args, **kwargs)
@_retry(OperationalError) # retry on deadlock
def delete(self, *args, **kwargs) -> None:
@ -81,7 +81,7 @@ class OrderedModel(models.Model):
super().delete(*args, **kwargs)
@_retry((IntegrityError, OperationalError)) # retry on duplicate order or deadlock
def _save_no_order_provided(self) -> None:
def _save_no_order_provided(self, *args, **kwargs) -> None:
"""
Save self to DB without an order provided (e.g on creation).
Order is set to the next available order, or 0 if there are no other instances.
@ -97,7 +97,7 @@ class OrderedModel(models.Model):
instances = self._lock_ordering_queryset() # lock ordering queryset to prevent reading inconsistent data
max_order = max(instance.order for instance in instances) if instances else -1
self.order = max_order + 1
super().save()
super().save(*args, **kwargs)
@_retry(OperationalError) # retry on deadlock
def to(self, order: int) -> None:

View file

@ -32,7 +32,6 @@ EVENT_SUBTYPE_MESSAGE_CHANGED = "message_changed"
EVENT_SUBTYPE_MESSAGE_DELETED = "message_deleted"
EVENT_SUBTYPE_BOT_MESSAGE = "bot_message"
EVENT_SUBTYPE_THREAD_BROADCAST = "thread_broadcast"
EVENT_SUBTYPE_FILE_SHARE = "file_share"
EVENT_TYPE_CHANNEL_DELETED = "channel_deleted"
EVENT_TYPE_CHANNEL_CREATED = "channel_created"
EVENT_TYPE_CHANNEL_RENAMED = "channel_rename"

View file

@ -1,9 +1,7 @@
import json
import logging
from django.apps import apps
from apps.integrations.tasks import create_alert
from apps.slack.scenarios import scenario_step
logger = logging.getLogger(__name__)
@ -30,9 +28,6 @@ class SlackChannelMessageEventStep(scenario_step.ScenarioStep):
and "thread_ts" in payload["event"]["previous_message"]
):
self.delete_thread_message_from_resolution_note(slack_user_identity, payload)
# Otherwise check if it is a message from channel with Slack Channel Integration
else:
self.create_alert_for_slack_channel_integration_if_needed(payload)
def save_thread_message_for_resolution_note(self, slack_user_identity, payload):
ResolutionNoteSlackMessage = apps.get_model("alerts", "ResolutionNoteSlackMessage")
@ -144,37 +139,6 @@ class SlackChannelMessageEventStep(scenario_step.ScenarioStep):
slack_thread_message.delete()
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
def create_alert_for_slack_channel_integration_if_needed(self, payload):
if "subtype" in payload["event"] and payload["event"]["subtype"] != scenario_step.EVENT_SUBTYPE_FILE_SHARE:
return
AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel")
alert_receive_channels = AlertReceiveChannel.objects.filter(
integration_slack_channel_id=payload["event"]["channel"], organization=self.organization
).all()
for alert_receive_channel in alert_receive_channels:
r = self._slack_client.api_call(
"chat.getPermalink",
channel=payload["event"]["channel"],
message_ts=payload["event"]["ts"],
)
# insert permalink to payload to have access to it in templaters
payload["event"]["amixr_mixin"] = {"permalink": r["permalink"]}
create_alert.apply_async(
[],
{
"title": "<#{}>".format(payload["event"]["channel"]),
"message": "{}\n_New message in <#{}> channel_".format(
payload["event"]["text"], payload["event"]["channel"]
),
"image_url": None,
"link_to_upstream_details": r["permalink"],
"alert_receive_channel_pk": alert_receive_channel.pk,
"integration_unique_data": json.dumps(payload["event"]),
"raw_request_data": payload["event"],
},
)
STEPS_ROUTING = [
{

View file

@ -28,7 +28,6 @@ from apps.slack.scenarios.profile_update import STEPS_ROUTING as PROFILE_UPDATE_
from apps.slack.scenarios.resolution_note import STEPS_ROUTING as RESOLUTION_NOTE_ROUTING
from apps.slack.scenarios.scenario_step import (
EVENT_SUBTYPE_BOT_MESSAGE,
EVENT_SUBTYPE_FILE_SHARE,
EVENT_SUBTYPE_MESSAGE_CHANGED,
EVENT_SUBTYPE_MESSAGE_DELETED,
EVENT_TYPE_APP_MENTION,
@ -334,7 +333,6 @@ class SlackEventApiEndpointView(APIView):
in [
EVENT_SUBTYPE_BOT_MESSAGE,
EVENT_SUBTYPE_MESSAGE_CHANGED,
EVENT_SUBTYPE_FILE_SHARE,
EVENT_SUBTYPE_MESSAGE_DELETED,
]
)

View file

@ -19,4 +19,4 @@ frontend_enterprise
/test-results/
/playwright-report/
/playwright/.cache/
storageState.json
/integration-tests/storageState.json

View file

@ -1,20 +1,39 @@
import { chromium, FullConfig, expect, Page } from '@playwright/test';
import { test as setup, chromium, FullConfig, expect, Page, BrowserContext, APIResponse } from '@playwright/test';
import { BASE_URL, GRAFANA_PASSWORD, GRAFANA_USERNAME, IS_OPEN_SOURCE, ONCALL_API_URL } from './utils/constants';
import { clickButton, getInputByName } from './utils/forms';
import { goToGrafanaPage } from './utils/navigation';
import { STORAGE_STATE } from '../playwright.config';
const IS_CLOUD = !IS_OPEN_SOURCE;
const GLOBAL_SETUP_RETRIES = 3;
const makeGrafanaLoginRequest = async (browserContext: BrowserContext): Promise<APIResponse> =>
browserContext.request.post(`${BASE_URL}/login`, {
data: {
user: GRAFANA_USERNAME,
password: GRAFANA_PASSWORD,
},
});
const pollGrafanaInstanceUntilItIsHealthy = async (browserContext: BrowserContext): Promise<boolean> => {
console.log('Polling the grafana instance to make sure it is healthy');
const res = await makeGrafanaLoginRequest(browserContext);
if (!res.ok()) {
console.log(`Grafana instance is unavailable. Got HTTP ${res.status()}. Will wait 5 seconds and then try again`);
await new Promise((resolve) => setTimeout(resolve, 5000));
return pollGrafanaInstanceUntilItIsHealthy(browserContext);
}
console.log('Grafana instance is available');
return true;
};
/**
* go to config page and wait for plugin icon to be available on left-hand navigation
*/
const configureOnCallPlugin = async (page: Page): Promise<void> => {
// plugin configuration can safely be skipped for non open-source environments
if (!IS_OPEN_SOURCE) {
return;
}
/**
* go to the oncall plugin configuration page and wait for the page to be loaded
*/
@ -52,19 +71,26 @@ const globalSetup = async (config: FullConfig): Promise<void> => {
const browser = await chromium.launch({ headless, slowMo: headless ? 0 : 100 });
const browserContext = await browser.newContext();
const res = await browserContext.request.post(`${BASE_URL}/login`, {
data: {
user: GRAFANA_USERNAME,
password: GRAFANA_PASSWORD,
},
});
if (IS_CLOUD) {
/**
* check that the grafana instance is available. If HTTP 503 is returned it means the
* instance is currently unavailable. Poll until it is available
*/
await pollGrafanaInstanceUntilItIsHealthy(browserContext);
}
const res = await makeGrafanaLoginRequest(browserContext);
expect(res.ok()).toBeTruthy();
await browserContext.storageState({ path: './storageState.json' });
await browserContext.storageState({ path: STORAGE_STATE });
// make sure the plugin has been configured
const page = await browserContext.newPage();
await configureOnCallPlugin(page);
if (IS_OPEN_SOURCE) {
// plugin configuration can safely be skipped for cloud environments
await configureOnCallPlugin(page);
}
await browserContext.close();
};
@ -88,4 +114,13 @@ const globalSetupWithRetries = async (config: FullConfig): Promise<void> => {
await globalSetup(config);
};
export default globalSetupWithRetries;
setup('Configure Grafana OnCall plugin', async ({}, { config }) => {
/**
* Unconditionally marks the setup as "slow", giving it triple the default timeout.
* This is mostly useful for the rare case for Cloud Grafana instances where the instance may be down/unavailable
* and we need to poll it until it is available
*/
setup.slow();
await globalSetupWithRetries(config);
});

View file

@ -1,3 +1,5 @@
import path from 'path';
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';
@ -7,12 +9,13 @@ import { devices } from '@playwright/test';
*/
require('dotenv').config();
export const STORAGE_STATE = path.join(__dirname, 'integration-tests/storageState.json');
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './integration-tests',
globalSetup: './integration-tests/globalSetup.ts',
/* Maximum time one test can run for. */
timeout: 60 * 1000,
expect: {
@ -38,8 +41,6 @@ const config: PlaywrightTestConfig = {
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
storageState: './storageState.json',
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
@ -53,23 +54,33 @@ const config: PlaywrightTestConfig = {
/* Configure projects for major browsers */
projects: [
{
name: 'setup',
testMatch: /globalSetup\.ts/,
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: STORAGE_STATE,
},
dependencies: ['setup'],
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
storageState: STORAGE_STATE,
},
dependencies: ['setup'],
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
storageState: STORAGE_STATE,
},
dependencies: ['setup'],
},
/* Test against mobile viewports. */