Skip to content

Commit

Permalink
feat: activations with webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
mkanoor committed Mar 5, 2024
1 parent c30db22 commit d35c4dc
Show file tree
Hide file tree
Showing 25 changed files with 2,170 additions and 6 deletions.
142 changes: 141 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ django-ansible-base = { git = "https://github.com/ansible/django-ansible-base.gi
jinja2 = ">=3.1.3,<3.2"
django-split-settings = "^1.2.0"
pexpect = "^4.9.0"
psycopg = "*"
xxhash = "*"

[tool.poetry.group.test.dependencies]
pytest = "*"
Expand Down
29 changes: 29 additions & 0 deletions src/aap_eda/api/filters/webhook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2024 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import django_filters

from aap_eda.core import models


class WebhookFilter(django_filters.FilterSet):
name = django_filters.CharFilter(
field_name="name",
lookup_expr="istartswith",
label="Filter by webhook name.",
)

class Meta:
model = models.Webhook
fields = ["name"]
4 changes: 4 additions & 0 deletions src/aap_eda/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
UserListSerializer,
UserSerializer,
)
from .webhook import WebhookInSerializer, WebhookOutSerializer

__all__ = (
# auth
Expand Down Expand Up @@ -119,4 +120,7 @@
"EventStreamSerializer",
"EventStreamCreateSerializer",
"EventStreamOutSerializer",
# webhooks
"WebhookInSerializer",
"WebhookOutSerializer",
)
50 changes: 48 additions & 2 deletions src/aap_eda/api/serializers/activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
substitute_source_args,
swap_sources,
)
from aap_eda.api.serializers.webhook import WebhookOutSerializer
from aap_eda.core import models, validators
from aap_eda.core.enums import ProcessParentType

Expand All @@ -44,7 +45,7 @@ def _updated_ruleset(validated_data):
try:
sources_info = []

for event_stream_id in validated_data["event_streams"]:
for event_stream_id in validated_data.get("event_streams", []):
event_stream = models.EventStream.objects.get(id=event_stream_id)

if event_stream.rulebook:
Expand All @@ -66,6 +67,18 @@ def _updated_ruleset(validated_data):
)
sources_info.append(source)

if validated_data.get("webhooks", []):
sources_info = [
{
"name": "webhook_event_stream",
"type": "ansible.eda.pg_listener",
"args": {
"dsn": "{{ EDA_PG_NOTIFY_DSN }}",
"channels": "{{ EDA_WEBHOOK_CHANNELS }}",
},
}
]

return swap_sources(validated_data["rulebook_rulesets"], sources_info)
except Exception as e:
logger.error(f"Failed to update rulesets: {e}")
Expand All @@ -87,6 +100,12 @@ class ActivationSerializer(serializers.ModelSerializer):
child=EventStreamOutSerializer(),
)

webhooks = serializers.ListField(
required=False,
allow_null=True,
child=WebhookOutSerializer(),
)

class Meta:
model = models.Activation
fields = [
Expand All @@ -112,6 +131,7 @@ class Meta:
"credentials",
"event_streams",
"log_level",
"webhooks",
]
read_only_fields = [
"id",
Expand All @@ -137,6 +157,11 @@ class ActivationListSerializer(serializers.ModelSerializer):
allow_null=True,
child=EventStreamOutSerializer(),
)
webhooks = serializers.ListField(
required=False,
allow_null=True,
child=WebhookOutSerializer(),
)

class Meta:
model = models.Activation
Expand All @@ -163,6 +188,7 @@ class Meta:
"credentials",
"event_streams",
"log_level",
"webhooks",
]
read_only_fields = ["id", "created_at", "modified_at"]

Expand All @@ -178,6 +204,10 @@ def to_representation(self, activation):
EventStreamOutSerializer(event_stream).data
for event_stream in activation.event_streams.all()
]
webhooks = [
WebhookOutSerializer(webhook).data
for webhook in activation.webhooks.all()
]

return {
"id": activation.id,
Expand All @@ -202,6 +232,7 @@ def to_representation(self, activation):
"credentials": credentials,
"event_streams": event_streams,
"log_level": activation.log_level,
"webhooks": webhooks,
}


Expand All @@ -223,6 +254,7 @@ class Meta:
"credentials",
"event_streams",
"log_level",
"webhooks",
]

rulebook_id = serializers.IntegerField(
Expand Down Expand Up @@ -254,6 +286,12 @@ class Meta:
child=serializers.IntegerField(),
validators=[validators.check_if_event_streams_exists],
)
webhooks = serializers.ListField(
required=False,
allow_null=True,
child=serializers.IntegerField(),
validators=[validators.check_if_webhooks_exists],
)

def validate(self, data):
user = data["user"]
Expand All @@ -277,7 +315,9 @@ def create(self, validated_data):
validated_data["rulebook_rulesets"] = rulebook.rulesets
validated_data["git_hash"] = rulebook.project.git_hash
validated_data["project_id"] = rulebook.project.id
if validated_data.get("event_streams"):
if validated_data.get("event_streams") or validated_data.get(
"webhooks"
):
validated_data["rulebook_rulesets"] = _updated_ruleset(
validated_data
)
Expand Down Expand Up @@ -359,6 +399,7 @@ class Meta:
"credentials",
"event_streams",
"log_level",
"webhooks",
]
read_only_fields = ["id", "created_at", "modified_at", "restarted_at"]

Expand Down Expand Up @@ -409,6 +450,10 @@ def to_representation(self, activation):
EventStreamOutSerializer(event_stream).data
for event_stream in activation.event_streams.all()
]
webhooks = [
WebhookOutSerializer(webhook).data
for webhook in activation.webhooks.all()
]

return {
"id": activation.id,
Expand Down Expand Up @@ -438,6 +483,7 @@ def to_representation(self, activation):
"credentials": credentials,
"event_streams": event_streams,
"log_level": activation.log_level,
"webhooks": webhooks,
}


Expand Down
91 changes: 91 additions & 0 deletions src/aap_eda/api/serializers/webhook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright 2024 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from rest_framework import serializers

from aap_eda.core import models, validators


class WebhookInSerializer(serializers.ModelSerializer):
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
hmac_algorithm = serializers.CharField(
default="sha256",
help_text="Hash algorithm to use",
validators=[validators.valid_hash_algorithm],
)
hmac_format = serializers.CharField(
default="hex",
help_text="Hash format to use, hex or base64",
validators=[validators.valid_hash_format],
)
auth_type = serializers.CharField(
default="hmac",
help_text="Auth type to use hmac or token or basic",
validators=[validators.valid_webhook_auth_type],
)
additional_data_headers = serializers.ListField(
required=False,
allow_null=True,
child=serializers.CharField(),
)

class Meta:
model = models.Webhook
fields = [
"name",
"type",
"hmac_algorithm",
"header_key",
"auth_type",
"hmac_signature_prefix",
"hmac_format",
"owner",
"secret",
"username",
"test_mode",
"additional_data_headers",
]


class WebhookOutSerializer(serializers.ModelSerializer):
owner = serializers.SerializerMethodField()

class Meta:
model = models.Webhook
read_only_fields = [
"id",
"owner",
"url",
"type",
"created_at",
"modified_at",
"test_content_type",
"test_content",
"test_error_message",
]
fields = [
"name",
"test_mode",
"hmac_algorithm",
"header_key",
"hmac_signature_prefix",
"hmac_format",
"auth_type",
"additional_data_headers",
"username",
*read_only_fields,
]

def get_owner(self, obj) -> str:
return f"{obj.owner.username}"
5 changes: 5 additions & 0 deletions src/aap_eda/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
)
router.register("credentials", views.CredentialViewSet)
router.register("decision-environments", views.DecisionEnvironmentViewSet)
router.register("webhooks", views.WebhookViewSet)
router.register(
"external_webhook",
views.ExternalWebhookViewSet,
)

openapi_urls = [
path(
Expand Down
6 changes: 6 additions & 0 deletions src/aap_eda/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
from .credential import CredentialViewSet
from .decision_environment import DecisionEnvironmentViewSet
from .event_stream import EventStreamViewSet
from .external_webhook import ExternalWebhookViewSet
from .project import ExtraVarViewSet, ProjectViewSet
from .rulebook import AuditRuleViewSet, RulebookViewSet
from .user import CurrentUserAwxTokenViewSet, CurrentUserView, UserViewSet
from .webhook import WebhookViewSet

__all__ = (
# auth
Expand All @@ -44,4 +46,8 @@
"DecisionEnvironmentViewSet",
# event_stream
"EventStreamViewSet",
# webhook
"WebhookViewSet",
# External webhook
"ExternalWebhookViewSet",
)
23 changes: 23 additions & 0 deletions src/aap_eda/api/views/activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
# limitations under the License.
import logging

import yaml
from django.conf import settings
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as defaultfilters
from drf_spectacular.utils import (
Expand Down Expand Up @@ -84,6 +86,27 @@ def create(self, request):
serializer.is_valid(raise_exception=True)

activation = serializer.create(serializer.validated_data)
if activation.webhooks:
extra_var = {}
if activation.extra_var_id:
extra_var_obj = models.ExtraVar.objects.get(
pk=activation.extra_var_id
)
extra_var = yaml.safe_load(extra_var_obj.extra_var)

extra_var["EDA_WEBHOOK_CHANNELS"] = [
webhook.channel_name for webhook in activation.webhooks.all()
]
extra_var["EDA_PG_NOTIFY_DSN"] = settings.PG_NOTIFY_DSN
if activation.extra_var_id and extra_var_obj:
extra_var_obj.extra_var = yaml.dump(extra_var)
extra_var_obj.save()
else:
extra_var_obj = models.ExtraVar.objects.create(
extra_var=yaml.dump(extra_var)
)
activation.extra_var_id = extra_var_obj.id
activation.save(update_fields=["extra_var_id"])

if activation.is_enabled:
start_rulebook_process(
Expand Down
1 change: 0 additions & 1 deletion src/aap_eda/api/views/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ def get_serializer_class(self):
def retrieve(self, _request, pk=None):
# TODO: Optimization by querying to retrieve desired permission format
role = get_object_or_404(self.queryset, pk=pk)

detail_serialzer = self.get_serializer_class()
role = detail_serialzer(role).data
result = display_permissions(role)
Expand Down

0 comments on commit d35c4dc

Please sign in to comment.