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 Apr 26, 2024
1 parent 60a23fa commit 6c66371
Show file tree
Hide file tree
Showing 27 changed files with 2,131 additions and 4 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 @@ -51,6 +51,8 @@ jinja2 = ">=3.1.3,<3.2"
django-split-settings = "^1.2.0"
pexpect = "^4.9.0"
ansible-runner = ">=2.3"
psycopg = "^3.1.17"
xxhash = "*"

[tool.poetry.group.test.dependencies]
pytest = "*"
Expand Down
7 changes: 7 additions & 0 deletions src/aap_eda/api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,10 @@ class InvalidEventStreamRulebook(APIException):
default_detail = (
"Configuration Error: Event stream template rulebook is invalid"
)


class InvalidWebhookSource(APIException):
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
default_detail = (
"Configuration Error: Webhook source could not be upated in ruleset"
)
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 @@ -78,6 +78,7 @@
UserListSerializer,
UserSerializer,
)
from .webhook import WebhookInSerializer, WebhookOutSerializer

__all__ = (
# auth
Expand Down Expand Up @@ -131,4 +132,7 @@
"TeamCreateSerializer",
"TeamUpdateSerializer",
"TeamDetailSerializer",
# webhooks
"WebhookInSerializer",
"WebhookOutSerializer",
)
85 changes: 84 additions & 1 deletion src/aap_eda/api/serializers/activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
EDA_SERVER_VAULT_LABEL,
PG_NOTIFY_TEMPLATE_RULEBOOK_DATA,
)
from aap_eda.api.exceptions import InvalidEventStreamRulebook
from aap_eda.api.exceptions import (
InvalidEventStreamRulebook,
InvalidWebhookSource,
)
from aap_eda.api.serializers.decision_environment import (
DecisionEnvironmentRefSerializer,
)
Expand All @@ -39,6 +42,7 @@
ProjectRefSerializer,
)
from aap_eda.api.serializers.rulebook import RulebookRefSerializer
from aap_eda.api.serializers.webhook import WebhookOutSerializer
from aap_eda.api.vault import encrypt_string
from aap_eda.core import models, validators
from aap_eda.core.enums import DefaultCredentialType, ProcessParentType
Expand All @@ -63,6 +67,36 @@ class VaultData:
password_used: bool = False


def _update_webhook_source(validated_data: dict, vault_data: VaultData):
try:
vault_data.password_used = True
encrypted_dsn = encrypt_string(
password=vault_data.password,
plaintext=settings.PG_NOTIFY_DSN,
vault_id=EDA_SERVER_VAULT_LABEL,
)

channels = []
for webhook_id in validated_data.get("webhooks"):
obj = models.Webhook.objects.get(id=webhook_id)
channels.append(obj.channel_name)

sources_info = [
{
"name": "webhook_event_stream",
"type": "ansible.eda.pg_listener",
"args": {
"dsn": encrypted_dsn,
"channels": channels,
},
}
]
return swap_sources(validated_data["rulebook_rulesets"], sources_info)
except Exception as e:
logger.error(f"Failed to update webhook source in rulesets: {e}")
raise InvalidWebhookSource(e)


def _updated_ruleset(validated_data: dict, vault_data: VaultData):
try:
sources_info = []
Expand Down Expand Up @@ -215,6 +249,12 @@ class ActivationSerializer(serializers.ModelSerializer):
child=EdaCredentialSerializer(),
)

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

class Meta:
model = models.Activation
fields = [
Expand All @@ -241,6 +281,7 @@ class Meta:
"event_streams",
"eda_credentials",
"log_level",
"webhooks",
]
read_only_fields = [
"id",
Expand Down Expand Up @@ -271,6 +312,11 @@ class ActivationListSerializer(serializers.ModelSerializer):
allow_null=True,
help_text="Service name of the activation",
)
webhooks = serializers.ListField(
required=False,
allow_null=True,
child=WebhookOutSerializer(),
)

class Meta:
model = models.Activation
Expand Down Expand Up @@ -299,6 +345,7 @@ class Meta:
"log_level",
"eda_credentials",
"k8s_service_name",
"webhooks",
]
read_only_fields = ["id", "created_at", "modified_at"]

Expand All @@ -319,6 +366,10 @@ def to_representation(self, activation):
if activation.extra_var
else None
)
webhooks = [
WebhookOutSerializer(webhook).data
for webhook in activation.webhooks.all()
]

return {
"id": activation.id,
Expand All @@ -345,6 +396,7 @@ def to_representation(self, activation):
"log_level": activation.log_level,
"eda_credentials": eda_credentials,
"k8s_service_name": activation.k8s_service_name,
"webhooks": webhooks,
}


Expand All @@ -368,6 +420,7 @@ class Meta:
"log_level",
"eda_credentials",
"k8s_service_name",
"webhooks",
]

organization_id = serializers.IntegerField(
Expand Down Expand Up @@ -409,6 +462,12 @@ class Meta:
allow_null=True,
validators=[validators.check_if_rfc_1035_compliant],
)
webhooks = serializers.ListField(
required=False,
allow_null=True,
child=serializers.IntegerField(),
validators=[validators.check_if_webhooks_exists],
)

def validate(self, data):
_validate_credentials_and_token_and_rulebook(data=data, creating=True)
Expand Down Expand Up @@ -436,6 +495,11 @@ def create(self, validated_data):
validated_data, vault_data
)

if validated_data.get("webhooks", []):
validated_data["rulebook_rulesets"] = _update_webhook_source(
validated_data, vault_data
)

vault = _get_vault_credential_type()

if validated_data.get("eda_credentials"):
Expand Down Expand Up @@ -524,6 +588,11 @@ class ActivationReadSerializer(serializers.ModelSerializer):
allow_null=True,
help_text="Service name of the activation",
)
webhooks = serializers.ListField(
required=False,
allow_null=True,
child=WebhookOutSerializer(),
)

class Meta:
model = models.Activation
Expand Down Expand Up @@ -555,6 +624,7 @@ class Meta:
"event_streams",
"log_level",
"k8s_service_name",
"webhooks",
]
read_only_fields = ["id", "created_at", "modified_at", "restarted_at"]

Expand Down Expand Up @@ -609,6 +679,10 @@ def to_representation(self, activation):
if activation.extra_var
else None
)
webhooks = [
WebhookOutSerializer(webhook).data
for webhook in activation.webhooks.all()
]

return {
"id": activation.id,
Expand Down Expand Up @@ -640,6 +714,7 @@ def to_representation(self, activation):
"log_level": activation.log_level,
"eda_credentials": eda_credentials,
"k8s_service_name": activation.k8s_service_name,
"webhooks": webhooks,
}


Expand Down Expand Up @@ -671,6 +746,12 @@ class PostActivationSerializer(serializers.ModelSerializer):
allow_null=True,
validators=[validators.check_if_rfc_1035_compliant],
)
webhooks = serializers.ListField(
required=False,
allow_null=True,
child=serializers.IntegerField(),
validators=[validators.check_if_webhooks_exists],
)

def validate(self, data):
_validate_credentials_and_token_and_rulebook(data=data, creating=False)
Expand All @@ -693,6 +774,7 @@ class Meta:
"rulebook_id",
"eda_credentials",
"k8s_service_name",
"webhooks",
]
read_only_fields = [
"id",
Expand All @@ -718,6 +800,7 @@ def is_activation_valid(activation: models.Activation) -> tuple[bool, str]:
data["eda_credentials"] = [
obj.id for obj in activation.eda_credentials.all()
]
data["webhooks"] = [obj.id for obj in activation.webhooks.all()]
serializer = PostActivationSerializer(data=data)

valid = serializer.is_valid()
Expand Down
104 changes: 104 additions & 0 deletions src/aap_eda/api/serializers/webhook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 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 typing import Optional

from rest_framework import serializers

from aap_eda.api.serializers.organization import OrganizationRefSerializer
from aap_eda.core import models, validators


class WebhookInSerializer(serializers.ModelSerializer):
organization_id = serializers.IntegerField(required=False, allow_null=True)
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",
"organization_id",
]


class WebhookOutSerializer(serializers.ModelSerializer):
owner = serializers.SerializerMethodField()
organization = 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",
"organization",
*read_only_fields,
]

def get_owner(self, obj) -> str:
return f"{obj.owner.username}"

def get_organization(self, obj) -> Optional[OrganizationRefSerializer]:
return (
OrganizationRefSerializer(obj.organization).data
if obj.organization
else None
)

0 comments on commit 6c66371

Please sign in to comment.