Skip to content

Commit 8acb8ac

Browse files
committed
Make owner fields not nullable
Previously, the owner field in Service, Project, Farm, and Sender models was nullable to accommodate legacy data during migration. This change enforces the owner field to be non-nullable, ensuring data consistency.
1 parent 72a5717 commit 8acb8ac

14 files changed

+106
-41
lines changed

promgen/management/commands/import-jobs.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.core.management.base import BaseCommand
77

88
from promgen import prometheus, util
9+
from promgen.middleware import get_current_user
910
from promgen.signals import (
1011
trigger_write_config,
1112
trigger_write_rules,
@@ -24,7 +25,7 @@ def handle(self, target_file, replace_shard, **kwargs):
2425
else:
2526
config = json.load(open(target_file), encoding="utf8")
2627

27-
imported, skipped = prometheus.import_config(config, replace_shard)
28+
imported, skipped = prometheus.import_config(config, get_current_user(), replace_shard)
2829

2930
if imported:
3031
counters = {key: len(imported[key]) for key in imported}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 4.2.11 on 2025-04-09 05:23
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
dependencies = [
10+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11+
("promgen", "0025_farm_owner"),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name="farm",
17+
name="owner",
18+
field=models.ForeignKey(
19+
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
20+
),
21+
),
22+
migrations.AlterField(
23+
model_name="project",
24+
name="owner",
25+
field=models.ForeignKey(
26+
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
27+
),
28+
),
29+
migrations.AlterField(
30+
model_name="sender",
31+
name="owner",
32+
field=models.ForeignKey(
33+
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
34+
),
35+
),
36+
migrations.AlterField(
37+
model_name="service",
38+
name="owner",
39+
field=models.ForeignKey(
40+
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
41+
),
42+
),
43+
]

promgen/models.py

+4-10
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class Sender(models.Model):
8181
object_id = models.PositiveIntegerField()
8282
content_object = GenericForeignKey("content_type", "object_id")
8383

84-
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
84+
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
8585

8686
enabled = models.BooleanField(default=True)
8787

@@ -211,9 +211,7 @@ def __str__(self):
211211
class Service(models.Model):
212212
name = models.CharField(max_length=128, unique=True, validators=[validators.labelvalue])
213213
description = models.TextField(blank=True)
214-
owner = models.ForeignKey(
215-
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, default=None
216-
)
214+
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
217215

218216
notifiers = GenericRelation(Sender)
219217
rule_set = GenericRelation("Rule")
@@ -242,9 +240,7 @@ def default(cls, service_name="Default", shard_name="Default"):
242240
class Project(models.Model):
243241
name = models.CharField(max_length=128, unique=True, validators=[validators.labelvalue])
244242
description = models.TextField(blank=True)
245-
owner = models.ForeignKey(
246-
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, default=None
247-
)
243+
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
248244

249245
service = models.ForeignKey("promgen.Service", on_delete=models.CASCADE)
250246
shard = models.ForeignKey("promgen.Shard", on_delete=models.CASCADE)
@@ -266,9 +262,7 @@ def __str__(self):
266262
class Farm(models.Model):
267263
name = models.CharField(max_length=128, validators=[validators.labelvalue])
268264
source = models.CharField(max_length=128)
269-
owner = models.ForeignKey(
270-
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, default=None
271-
)
265+
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
272266

273267
class Meta:
274268
ordering = ["name"]

promgen/prometheus.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def import_rules_v2(config, content_object=None):
184184
return dict(counters)
185185

186186

187-
def import_config(config, replace_shard=None):
187+
def import_config(config, user, replace_shard=None):
188188
counters = collections.defaultdict(list)
189189
skipped = collections.defaultdict(list)
190190
for entry in config:
@@ -202,6 +202,7 @@ def import_config(config, replace_shard=None):
202202

203203
service, created = models.Service.objects.get_or_create(
204204
name=entry["labels"]["service"],
205+
owner=user,
205206
)
206207
if created:
207208
logger.debug("Created service %s", service)
@@ -212,6 +213,7 @@ def import_config(config, replace_shard=None):
212213
farm, created = models.Farm.objects.get_or_create(
213214
name=entry["labels"]["farm"],
214215
defaults={"source": entry["labels"].get("__farm_source", "pmc")},
216+
owner=user,
215217
)
216218
if created:
217219
logger.debug("Created farm %s", farm)
@@ -224,6 +226,7 @@ def import_config(config, replace_shard=None):
224226
service=service,
225227
shard=shard,
226228
defaults={"farm": farm},
229+
owner=user,
227230
)
228231
if created:
229232
logger.debug("Created project %s", project)

promgen/signals.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def add_user_to_default_group(instance, created, **kwargs):
281281
def add_email_sender(instance, created, **kwargs):
282282
if instance.email:
283283
models.Sender.objects.get_or_create(
284-
obj=instance, sender="promgen.notification.email", value=instance.email
284+
obj=instance, sender="promgen.notification.email", value=instance.email, owner=instance
285285
)
286286
else:
287287
logger.warning("No email for user %s", instance)

promgen/tests/notification/test_email.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ class EmailTest(tests.PromgenTest):
1313
def setUp(self):
1414
one = models.Project.objects.get(pk=1)
1515
two = models.Project.objects.get(pk=2)
16+
self.user = self.force_login(username="demo")
1617

17-
NotificationEmail.create(obj=one, value="[email protected]")
18-
NotificationEmail.create(obj=one, value="[email protected]")
19-
NotificationEmail.create(obj=two, value="[email protected]")
18+
NotificationEmail.create(obj=one, value="[email protected]", owner=self.user)
19+
NotificationEmail.create(obj=one, value="[email protected]", owner=self.user)
20+
NotificationEmail.create(obj=two, value="[email protected]", owner=self.user)
2021

2122
@override_settings(PROMGEN=tests.SETTINGS)
2223
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)

promgen/tests/notification/test_linenotify.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ class LineNotifyTest(tests.PromgenTest):
1313
def setUp(self):
1414
one = models.Project.objects.get(pk=1)
1515
two = models.Service.objects.get(pk=2)
16+
self.user = self.force_login(username="demo")
1617

17-
NotificationLineNotify.create(obj=one, value="hogehoge")
18-
NotificationLineNotify.create(obj=two, value="asdfasdf")
18+
NotificationLineNotify.create(obj=one, value="hogehoge", owner=self.user)
19+
NotificationLineNotify.create(obj=two, value="asdfasdf", owner=self.user)
1920

2021
@override_settings(PROMGEN=tests.SETTINGS)
2122
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)

promgen/tests/notification/test_slack.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ class SlackTest(tests.PromgenTest):
1616
def setUp(self):
1717
one = models.Project.objects.get(pk=1)
1818
two = models.Service.objects.get(pk=2)
19+
self.user = self.force_login(username="demo")
1920

20-
NotificationSlack.create(obj=one, value=self.TestHook1)
21-
NotificationSlack.create(obj=two, value=self.TestHook2)
21+
NotificationSlack.create(obj=one, value=self.TestHook1, owner=self.user)
22+
NotificationSlack.create(obj=two, value=self.TestHook2, owner=self.user)
2223

2324
@override_settings(PROMGEN=tests.SETTINGS)
2425
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)

promgen/tests/notification/test_user_email.py

+15-8
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,19 @@
1212

1313

1414
class UserSplayTest(tests.PromgenTest):
15+
def setUp(self):
16+
self.user = self.force_login(username="demo")
17+
1518
@override_settings(PROMGEN=tests.SETTINGS)
1619
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
1720
@mock.patch("promgen.notification.email.send_mail")
1821
@mock.patch("promgen.util.post")
1922
def test_user_splay(self, mock_email, mock_post):
2023
one = models.Service.objects.get(pk=1)
2124

22-
NotificationUser.create(obj=one, value=one.owner.username)
23-
NotificationLineNotify.create(obj=one.owner, value="#foo")
24-
NotificationEmail.create(obj=one.owner, value="[email protected]")
25+
NotificationUser.create(obj=one, value=one.owner.username, owner=self.user)
26+
NotificationLineNotify.create(obj=one.owner, value="#foo", owner=self.user)
27+
NotificationEmail.create(obj=one.owner, value="[email protected]", owner=self.user)
2528

2629
response = self.fireAlert()
2730
self.assertRoute(response, rest.AlertReceiver, 202)
@@ -41,8 +44,8 @@ def test_failed_user(self, mock_email):
4144
# The invalid one should be skipped while still letting
4245
# the valid one pass
4346
one = models.Service.objects.get(pk=1)
44-
NotificationEmail.create(obj=one, value="[email protected]")
45-
NotificationUser.create(obj=one, value="does not exist")
47+
NotificationEmail.create(obj=one, value="[email protected]", owner=self.user)
48+
NotificationUser.create(obj=one, value="does not exist", owner=self.user)
4649

4750
response = self.fireAlert()
4851
self.assertRoute(response, rest.AlertReceiver, 202)
@@ -58,11 +61,15 @@ def test_enabled(self, mock_email):
5861
one = models.Service.objects.get(pk=1)
5962

6063
# This notification is direct and disabled
61-
NotificationEmail.create(obj=one, value="[email protected]", enabled=False)
64+
NotificationEmail.create(
65+
obj=one, value="[email protected]", enabled=False, owner=self.user
66+
)
6267
# Our parent notification is enabled
63-
NotificationUser.create(obj=one, value=one.owner.username)
68+
NotificationUser.create(obj=one, value=one.owner.username, owner=self.user)
6469
# But the child notifier is disabled and shouldn't fire
65-
NotificationEmail.create(obj=one.owner, value="[email protected]", enabled=False)
70+
NotificationEmail.create(
71+
obj=one.owner, value="[email protected]", enabled=False, owner=self.user
72+
)
6673

6774
response = self.fireAlert()
6875
self.assertRoute(response, rest.AlertReceiver, 202)

promgen/tests/notification/test_webhook.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ def setUp(self):
1616
two = models.Service.objects.get(pk=1)
1717

1818
self.senderA = NotificationWebhook.create(
19-
obj=one, value="http://webhook.example.com/project"
19+
obj=one, value="http://webhook.example.com/project", owner_id=1
2020
)
2121
self.senderB = NotificationWebhook.create(
22-
obj=two, value="http://webhook.example.com/service"
22+
obj=two, value="http://webhook.example.com/service", owner_id=1
2323
)
2424

2525
@override_settings(PROMGEN=tests.SETTINGS)

promgen/tests/test_cli.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
class CLITests(PromgenTest):
1414
fixtures = ["testcases.yaml", "extras.yaml"]
1515

16+
def setUp(self):
17+
self.user = self.force_login(username="demo")
18+
1619
@mock.patch("promgen.signals._trigger_write_config")
1720
def test_register_job(self, mock_signal):
1821
# Assert when project doesn't exist
@@ -38,14 +41,16 @@ def test_register_host(self, mock_signal):
3841
with self.assertRaises(CommandError):
3942
management.call_command("register-host", "missing-project", "cli.example.com")
4043

41-
project = models.Project.objects.create(name="cli-project", service_id=1, shard_id=1)
44+
project = models.Project.objects.create(
45+
name="cli-project", service_id=1, shard_id=1, owner=self.user
46+
)
4247

4348
# Still assert an error if there is no Farm
4449
with self.assertRaises(CommandError):
4550
management.call_command("register-host", "cli-project", "cli.example.com")
4651

4752
# Register farm and finally register host
48-
project.farm = models.Farm.objects.create(name="cli-farm")
53+
project.farm = models.Farm.objects.create(name="cli-farm", owner=self.user)
4954
project.save()
5055
management.call_command("register-host", "cli-project", "cli.example.com")
5156
self.assertCount(models.Host, 2, "Should be a new host registered (1 host from fixture)")

promgen/tests/test_routes.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ def test_replace(self, mock_write, mock_reload):
5959
@mock.patch("requests.get")
6060
def test_scrape(self, mock_get):
6161
shard = models.Shard.objects.create(name="test_scrape_shard")
62-
service = models.Service.objects.create(name="test_scrape_service")
63-
farm = models.Farm.objects.create(name="test_scrape_farm")
62+
service = models.Service.objects.create(name="test_scrape_service", owner=self.user)
63+
farm = models.Farm.objects.create(name="test_scrape_farm", owner=self.user)
6464
farm.host_set.create(name="example.com")
6565
project = models.Project.objects.create(
66-
name="test_scrape_project", service=service, shard=shard, farm=farm
66+
name="test_scrape_project", service=service, shard=shard, farm=farm, owner=self.user
6767
)
6868

6969
# Uses the scrape target as the key, and the POST body that should

promgen/tests/test_signals.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@
77

88

99
class SignalTest(tests.PromgenTest):
10+
def setUp(self):
11+
self.user = self.force_login(username="demo")
12+
1013
@mock.patch("promgen.models.Audit.log")
1114
@mock.patch("promgen.signals.trigger_write_config.send")
1215
def test_write_signal(self, write_mock, log_mock):
1316
# Create a test farm
14-
farm = models.Farm.objects.create(name="farm")
17+
farm = models.Farm.objects.create(name="farm", owner=self.user)
1518
models.Host.objects.create(name="Host", farm=farm)
1619
# Create a new project or testing
17-
project = models.Project.objects.create(name="Project", service_id=1, farm=farm, shard_id=1)
20+
project = models.Project.objects.create(
21+
name="Project", service_id=1, farm=farm, shard_id=1, owner=self.user
22+
)
1823
e1 = models.Exporter.objects.create(
1924
job="Exporter 1",
2025
port=1234,
@@ -35,10 +40,12 @@ def test_write_signal(self, write_mock, log_mock):
3540
@mock.patch("promgen.signals.trigger_write_config.send")
3641
def test_write_and_delete(self, write_mock, log_mock):
3742
# Create a test farm
38-
farm = models.Farm.objects.create(name="farm")
43+
farm = models.Farm.objects.create(name="farm", owner=self.user)
3944
models.Host.objects.create(name="Host", farm=farm)
4045

41-
project = models.Project.objects.create(name="Project", service_id=1, farm=farm, shard_id=1)
46+
project = models.Project.objects.create(
47+
name="Project", service_id=1, farm=farm, shard_id=1, owner=self.user
48+
)
4249
# Farm but no exporters so no call
4350
self.assertEqual(write_mock.call_count, 0, "Should not be called without exporters")
4451

promgen/views.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1013,7 +1013,7 @@ def post(self, request, *args, **kwargs):
10131013
try:
10141014
body = json.loads(request.body.decode("utf-8"))
10151015

1016-
prometheus.import_config(body, **kwargs)
1016+
prometheus.import_config(body, self.request.user, **kwargs)
10171017
except Exception as e:
10181018
return HttpResponse(e, status=400)
10191019

@@ -1328,7 +1328,9 @@ def form_valid(self, form):
13281328
if data.get("shard"):
13291329
kwargs["replace_shard"] = data.get("shard")
13301330

1331-
imported, skipped = prometheus.import_config(json.loads(config), **kwargs)
1331+
imported, skipped = prometheus.import_config(
1332+
json.loads(config), self.request.user, **kwargs
1333+
)
13321334

13331335
if imported:
13341336
counters = {key: len(imported[key]) for key in imported}

0 commit comments

Comments
 (0)