Skip to content

Commit 511cf83

Browse files
committed
feat: activations with webhooks
1 parent 790198e commit 511cf83

25 files changed

+2170
-6
lines changed

poetry.lock

Lines changed: 141 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ django-ansible-base = { git = "https://github.com/ansible/django-ansible-base.gi
4949
jinja2 = ">=3.1.3,<3.2"
5050
django-split-settings = "^1.2.0"
5151
pexpect = "^4.9.0"
52+
psycopg = "*"
53+
xxhash = "*"
5254

5355
[tool.poetry.group.test.dependencies]
5456
pytest = "*"

src/aap_eda/api/filters/webhook.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright 2024 Red Hat, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import django_filters
16+
17+
from aap_eda.core import models
18+
19+
20+
class WebhookFilter(django_filters.FilterSet):
21+
name = django_filters.CharFilter(
22+
field_name="name",
23+
lookup_expr="istartswith",
24+
label="Filter by webhook name.",
25+
)
26+
27+
class Meta:
28+
model = models.Webhook
29+
fields = ["name"]

src/aap_eda/api/serializers/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
UserListSerializer,
7272
UserSerializer,
7373
)
74+
from .webhook import WebhookInSerializer, WebhookOutSerializer
7475

7576
__all__ = (
7677
# auth
@@ -119,4 +120,7 @@
119120
"EventStreamSerializer",
120121
"EventStreamCreateSerializer",
121122
"EventStreamOutSerializer",
123+
# webhooks
124+
"WebhookInSerializer",
125+
"WebhookOutSerializer",
122126
)

src/aap_eda/api/serializers/activation.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
substitute_source_args,
3535
swap_sources,
3636
)
37+
from aap_eda.api.serializers.webhook import WebhookOutSerializer
3738
from aap_eda.core import models, validators
3839
from aap_eda.core.enums import ProcessParentType
3940

@@ -44,7 +45,7 @@ def _updated_ruleset(validated_data):
4445
try:
4546
sources_info = []
4647

47-
for event_stream_id in validated_data["event_streams"]:
48+
for event_stream_id in validated_data.get("event_streams", []):
4849
event_stream = models.EventStream.objects.get(id=event_stream_id)
4950

5051
if event_stream.rulebook:
@@ -66,6 +67,18 @@ def _updated_ruleset(validated_data):
6667
)
6768
sources_info.append(source)
6869

70+
if validated_data.get("webhooks", []):
71+
sources_info = [
72+
{
73+
"name": "webhook_event_stream",
74+
"type": "ansible.eda.pg_listener",
75+
"args": {
76+
"dsn": "{{ EDA_PG_NOTIFY_DSN }}",
77+
"channels": "{{ EDA_WEBHOOK_CHANNELS }}",
78+
},
79+
}
80+
]
81+
6982
return swap_sources(validated_data["rulebook_rulesets"], sources_info)
7083
except Exception as e:
7184
logger.error(f"Failed to update rulesets: {e}")
@@ -87,6 +100,12 @@ class ActivationSerializer(serializers.ModelSerializer):
87100
child=EventStreamOutSerializer(),
88101
)
89102

103+
webhooks = serializers.ListField(
104+
required=False,
105+
allow_null=True,
106+
child=WebhookOutSerializer(),
107+
)
108+
90109
class Meta:
91110
model = models.Activation
92111
fields = [
@@ -112,6 +131,7 @@ class Meta:
112131
"credentials",
113132
"event_streams",
114133
"log_level",
134+
"webhooks",
115135
]
116136
read_only_fields = [
117137
"id",
@@ -137,6 +157,11 @@ class ActivationListSerializer(serializers.ModelSerializer):
137157
allow_null=True,
138158
child=EventStreamOutSerializer(),
139159
)
160+
webhooks = serializers.ListField(
161+
required=False,
162+
allow_null=True,
163+
child=WebhookOutSerializer(),
164+
)
140165

141166
class Meta:
142167
model = models.Activation
@@ -163,6 +188,7 @@ class Meta:
163188
"credentials",
164189
"event_streams",
165190
"log_level",
191+
"webhooks",
166192
]
167193
read_only_fields = ["id", "created_at", "modified_at"]
168194

@@ -178,6 +204,10 @@ def to_representation(self, activation):
178204
EventStreamOutSerializer(event_stream).data
179205
for event_stream in activation.event_streams.all()
180206
]
207+
webhooks = [
208+
WebhookOutSerializer(webhook).data
209+
for webhook in activation.webhooks.all()
210+
]
181211

182212
return {
183213
"id": activation.id,
@@ -202,6 +232,7 @@ def to_representation(self, activation):
202232
"credentials": credentials,
203233
"event_streams": event_streams,
204234
"log_level": activation.log_level,
235+
"webhooks": webhooks,
205236
}
206237

207238

@@ -223,6 +254,7 @@ class Meta:
223254
"credentials",
224255
"event_streams",
225256
"log_level",
257+
"webhooks",
226258
]
227259

228260
rulebook_id = serializers.IntegerField(
@@ -254,6 +286,12 @@ class Meta:
254286
child=serializers.IntegerField(),
255287
validators=[validators.check_if_event_streams_exists],
256288
)
289+
webhooks = serializers.ListField(
290+
required=False,
291+
allow_null=True,
292+
child=serializers.IntegerField(),
293+
validators=[validators.check_if_webhooks_exists],
294+
)
257295

258296
def validate(self, data):
259297
user = data["user"]
@@ -277,7 +315,9 @@ def create(self, validated_data):
277315
validated_data["rulebook_rulesets"] = rulebook.rulesets
278316
validated_data["git_hash"] = rulebook.project.git_hash
279317
validated_data["project_id"] = rulebook.project.id
280-
if validated_data.get("event_streams"):
318+
if validated_data.get("event_streams") or validated_data.get(
319+
"webhooks"
320+
):
281321
validated_data["rulebook_rulesets"] = _updated_ruleset(
282322
validated_data
283323
)
@@ -359,6 +399,7 @@ class Meta:
359399
"credentials",
360400
"event_streams",
361401
"log_level",
402+
"webhooks",
362403
]
363404
read_only_fields = ["id", "created_at", "modified_at", "restarted_at"]
364405

@@ -409,6 +450,10 @@ def to_representation(self, activation):
409450
EventStreamOutSerializer(event_stream).data
410451
for event_stream in activation.event_streams.all()
411452
]
453+
webhooks = [
454+
WebhookOutSerializer(webhook).data
455+
for webhook in activation.webhooks.all()
456+
]
412457

413458
return {
414459
"id": activation.id,
@@ -438,6 +483,7 @@ def to_representation(self, activation):
438483
"credentials": credentials,
439484
"event_streams": event_streams,
440485
"log_level": activation.log_level,
486+
"webhooks": webhooks,
441487
}
442488

443489

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Copyright 2024 Red Hat, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from rest_framework import serializers
16+
17+
from aap_eda.core import models, validators
18+
19+
20+
class WebhookInSerializer(serializers.ModelSerializer):
21+
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
22+
hmac_algorithm = serializers.CharField(
23+
default="sha256",
24+
help_text="Hash algorithm to use",
25+
validators=[validators.valid_hash_algorithm],
26+
)
27+
hmac_format = serializers.CharField(
28+
default="hex",
29+
help_text="Hash format to use, hex or base64",
30+
validators=[validators.valid_hash_format],
31+
)
32+
auth_type = serializers.CharField(
33+
default="hmac",
34+
help_text="Auth type to use hmac or token or basic",
35+
validators=[validators.valid_webhook_auth_type],
36+
)
37+
additional_data_headers = serializers.ListField(
38+
required=False,
39+
allow_null=True,
40+
child=serializers.CharField(),
41+
)
42+
43+
class Meta:
44+
model = models.Webhook
45+
fields = [
46+
"name",
47+
"type",
48+
"hmac_algorithm",
49+
"header_key",
50+
"auth_type",
51+
"hmac_signature_prefix",
52+
"hmac_format",
53+
"owner",
54+
"secret",
55+
"username",
56+
"test_mode",
57+
"additional_data_headers",
58+
]
59+
60+
61+
class WebhookOutSerializer(serializers.ModelSerializer):
62+
owner = serializers.SerializerMethodField()
63+
64+
class Meta:
65+
model = models.Webhook
66+
read_only_fields = [
67+
"id",
68+
"owner",
69+
"url",
70+
"type",
71+
"created_at",
72+
"modified_at",
73+
"test_content_type",
74+
"test_content",
75+
"test_error_message",
76+
]
77+
fields = [
78+
"name",
79+
"test_mode",
80+
"hmac_algorithm",
81+
"header_key",
82+
"hmac_signature_prefix",
83+
"hmac_format",
84+
"auth_type",
85+
"additional_data_headers",
86+
"username",
87+
*read_only_fields,
88+
]
89+
90+
def get_owner(self, obj) -> str:
91+
return f"{obj.owner.username}"

src/aap_eda/api/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
)
4545
router.register("credentials", views.CredentialViewSet)
4646
router.register("decision-environments", views.DecisionEnvironmentViewSet)
47+
router.register("webhooks", views.WebhookViewSet)
48+
router.register(
49+
"external_webhook",
50+
views.ExternalWebhookViewSet,
51+
)
4752

4853
openapi_urls = [
4954
path(

src/aap_eda/api/views/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
from .credential import CredentialViewSet
1818
from .decision_environment import DecisionEnvironmentViewSet
1919
from .event_stream import EventStreamViewSet
20+
from .external_webhook import ExternalWebhookViewSet
2021
from .project import ExtraVarViewSet, ProjectViewSet
2122
from .rulebook import AuditRuleViewSet, RulebookViewSet
2223
from .user import CurrentUserAwxTokenViewSet, CurrentUserView, UserViewSet
24+
from .webhook import WebhookViewSet
2325

2426
__all__ = (
2527
# auth
@@ -44,4 +46,8 @@
4446
"DecisionEnvironmentViewSet",
4547
# event_stream
4648
"EventStreamViewSet",
49+
# webhook
50+
"WebhookViewSet",
51+
# External webhook
52+
"ExternalWebhookViewSet",
4753
)

src/aap_eda/api/views/activation.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# limitations under the License.
1414
import logging
1515

16+
import yaml
17+
from django.conf import settings
1618
from django.shortcuts import get_object_or_404
1719
from django_filters import rest_framework as defaultfilters
1820
from drf_spectacular.utils import (
@@ -84,6 +86,27 @@ def create(self, request):
8486
serializer.is_valid(raise_exception=True)
8587

8688
activation = serializer.create(serializer.validated_data)
89+
if activation.webhooks:
90+
extra_var = {}
91+
if activation.extra_var_id:
92+
extra_var_obj = models.ExtraVar.objects.get(
93+
pk=activation.extra_var_id
94+
)
95+
extra_var = yaml.safe_load(extra_var_obj.extra_var)
96+
97+
extra_var["EDA_WEBHOOK_CHANNELS"] = [
98+
webhook.channel_name for webhook in activation.webhooks.all()
99+
]
100+
extra_var["EDA_PG_NOTIFY_DSN"] = settings.PG_NOTIFY_DSN
101+
if activation.extra_var_id and extra_var_obj:
102+
extra_var_obj.extra_var = yaml.dump(extra_var)
103+
extra_var_obj.save()
104+
else:
105+
extra_var_obj = models.ExtraVar.objects.create(
106+
extra_var=yaml.dump(extra_var)
107+
)
108+
activation.extra_var_id = extra_var_obj.id
109+
activation.save(update_fields=["extra_var_id"])
87110

88111
if activation.is_enabled:
89112
start_rulebook_process(

src/aap_eda/api/views/auth.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ def get_serializer_class(self):
138138
def retrieve(self, _request, pk=None):
139139
# TODO: Optimization by querying to retrieve desired permission format
140140
role = get_object_or_404(self.queryset, pk=pk)
141-
142141
detail_serialzer = self.get_serializer_class()
143142
role = detail_serialzer(role).data
144143
result = display_permissions(role)

0 commit comments

Comments
 (0)