Skip to content

Commit

Permalink
feat: allow to update activations via the patch API
Browse files Browse the repository at this point in the history
Activations must be in disabled mode. Most fields are editable.

AAP-37295: Edit an existing activation
  • Loading branch information
bzwei committed Jan 6, 2025
1 parent 0f2be72 commit a20f967
Show file tree
Hide file tree
Showing 10 changed files with 439 additions and 32 deletions.
2 changes: 2 additions & 0 deletions src/aap_eda/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ActivationListSerializer,
ActivationReadSerializer,
ActivationSerializer,
ActivationUpdateSerializer,
PostActivationSerializer,
)
from .auth import JWTTokenSerializer, LoginSerializer, RefreshTokenSerializer
Expand Down Expand Up @@ -100,6 +101,7 @@
"ActivationSerializer",
"ActivationListSerializer",
"ActivationCreateSerializer",
"ActivationUpdateSerializer",
"ActivationReadSerializer",
"ActivationInstanceSerializer",
"ActivationInstanceLogSerializer",
Expand Down
155 changes: 152 additions & 3 deletions src/aap_eda/api/serializers/activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,8 +459,6 @@ def create(self, validated_data):
validated_data, vault_data
)

vault = _get_vault_credential_type()

if validated_data.get("eda_credentials"):
_update_extra_vars_from_eda_credentials(
validated_data=validated_data,
Expand All @@ -473,13 +471,164 @@ def create(self, validated_data):
"eda_system_vault_credential"
] = _create_system_eda_credential(
vault_data.password,
vault,
_get_vault_credential_type(),
validated_data.get("organization_id"),
)

return super().create(validated_data)


class ActivationUpdateSerializer(serializers.ModelSerializer):
"""Serializer for updating the Activation."""

class Meta:
model = models.Activation
fields = [
"name",
"description",
"decision_environment_id",
"rulebook_id",
"extra_var",
"organization_id",
"user",
"restart_policy",
"awx_token_id",
"log_level",
"eda_credentials",
"k8s_service_name",
"source_mappings",
"skip_audit_events",
]

organization_id = serializers.IntegerField(
required=True,
allow_null=False,
validators=[validators.check_if_organization_exists],
)
rulebook_id = serializers.IntegerField(
validators=[validators.check_if_rulebook_exists]
)
extra_var = serializers.CharField(
required=False,
allow_null=True,
allow_blank=True,
validators=[validators.is_extra_var_dict],
)
decision_environment_id = serializers.IntegerField(
validators=[validators.check_if_de_exists]
)
awx_token_id = serializers.IntegerField(
allow_null=True,
validators=[validators.check_if_awx_token_exists],
required=False,
)
eda_credentials = serializers.ListField(
required=False,
allow_null=True,
child=serializers.IntegerField(),
validators=[
validators.check_multiple_credentials,
validators.check_single_aap_credential,
],
)
k8s_service_name = serializers.CharField(
required=False,
allow_null=True,
allow_blank=True,
validators=[validators.check_if_rfc_1035_compliant],
)

def validate(self, data):
_validate_credentials_and_token_and_rulebook(data=data, creating=True)
_validate_sources_with_event_streams(data=data)
return data

def prepare_update(self, activation: models.Activation):
rulebook_id = self.validated_data.get("rulebook_id")
self.validated_data["user_id"] = self.context["request"].user.id
if rulebook_id:
rulebook = models.Rulebook.objects.get(id=rulebook_id)
self.validated_data["rulebook_name"] = rulebook.name
self.validated_data["rulebook_rulesets"] = rulebook.rulesets
self.validated_data["git_hash"] = rulebook.project.git_hash
self.validated_data["project_id"] = rulebook.project.id

vault_data = VaultData()
if self.validated_data.get("source_mappings", []):
self.validated_data[
"rulebook_rulesets"
] = _update_event_stream_source(self.validated_data, vault_data)

if self.validated_data.get("eda_credentials"):
_update_extra_vars_from_eda_credentials(
validated_data=self.validated_data,
vault_data=vault_data,
creating=True,
)

if (
vault_data.password_used
and not activation.eda_system_vault_credential
):
self.validated_data[
"eda_system_vault_credential"
] = _create_system_eda_credential(
vault_data.password,
_get_vault_credential_type(),
self.validated_data.get(
"organization_id", activation.organization.id
),
)

def update(
self, instance: models.Activation, validated_data: dict
) -> None:
update_fields = []
eda_credentials = []
event_streams = []
for key, value in validated_data.items():
if key == "eda_credentials":
eda_credentials = value
continue
elif key == "event_streams":
event_streams = value
continue
setattr(instance, key, value)
update_fields.append(key)

instance.save(update_fields=update_fields)
if eda_credentials:
instance.eda_credentials.set(eda_credentials)
if event_streams:
instance.event_streams.set(event_streams)

def to_representation(self, activation):
extra_var = (
replace_vault_data(activation.extra_var)
if activation.extra_var
else None
)
eda_credentials = [
credential.id for credential in activation.eda_credentials.all()
]

return {
"name": activation.name,
"description": activation.description,
"decision_environment_id": activation.decision_environment_id,
"rulebook_id": activation.rulebook_id,
"extra_var": extra_var,
"organization_id": activation.organization_id,
"restart_policy": activation.restart_policy,
"awx_token_id": activation.awx_token_id,
"log_level": activation.log_level,
"eda_credentials": eda_credentials,
"k8s_service_name": activation.k8s_service_name,
"source_mappings": activation.source_mappings,
"skip_audit_events": activation.skip_audit_events,
}


class ActivationInstanceSerializer(serializers.ModelSerializer):
"""Serializer for the Activation Instance model."""

Expand Down
64 changes: 59 additions & 5 deletions src/aap_eda/api/views/activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,22 @@ class ActivationViewSet(
RedisDependencyMixin,
):
queryset = models.Activation.objects.all()
serializer_class = serializers.ActivationSerializer
filter_backends = (defaultfilters.DjangoFilterBackend,)
filterset_class = filters.ActivationFilter

rbac_action = None

def get_serializer_class(self):
if self.request.method == "PATCH":
return serializers.ActivationUpdateSerializer
elif (
self.request.method == "POST"
and self.request.path == "/api/eda/v1/activations/"
):
return serializers.ActivationCreateSerializer
else:
return serializers.ActivationReadSerializer

def filter_queryset(self, queryset):
if queryset.model is models.Activation:
return super().filter_queryset(
Expand All @@ -78,10 +88,7 @@ def filter_queryset(self, queryset):
| RedisDependencyMixin.redis_unavailable_response(),
)
def create(self, request):
context = {"request": request}
serializer = serializers.ActivationCreateSerializer(
data=request.data, context=context
)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)

# If we're expected to run this activation we need redis
Expand Down Expand Up @@ -125,6 +132,53 @@ def create(self, request):
status=status.HTTP_201_CREATED,
)

@extend_schema(
request=serializers.ActivationUpdateSerializer,
responses={
status.HTTP_200_OK: serializers.ActivationReadSerializer,
status.HTTP_400_BAD_REQUEST: OpenApiResponse(
description="Invalid data to update activation."
),
status.HTTP_409_CONFLICT: OpenApiResponse(
description="The activation is not allowed to be updated."
),
},
)
def partial_update(self, request, pk):
activation = self.get_object()
if activation.is_enabled:
return Response(
data="Activation is not in disabled mode",
status=status.HTTP_409_CONFLICT,
)
serializer = self.get_serializer(
instance=activation, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.prepare_update(activation)

old_data = model_to_dict(activation)
with transaction.atomic():
serializer.update(activation, serializer.validated_data)
check_related_permissions(
request.user,
serializer.Meta.model,
old_data,
model_to_dict(activation),
)

logger.info(
logging_utils.generate_simple_audit_log(
"Update",
resource_name,
activation.name,
activation.id,
activation.organization,
)
)

return Response(serializers.ActivationReadSerializer(activation).data)

@extend_schema(
description="Delete an existing Activation",
responses={
Expand Down
12 changes: 3 additions & 9 deletions src/aap_eda/core/management/commands/create_initial_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,7 @@
"inside of it"
),
"permissions": {
"activation": [
"add",
"view",
"delete",
"enable",
"disable",
"restart",
],
"activation": CRUD + ["enable", "disable", "restart"],
"rulebook_process": ["view"],
"audit_rule": ["view"],
"organization": ["view", "change", "delete"],
Expand All @@ -87,7 +80,7 @@
"a single organization"
),
"permissions": {
"activation": ["add", "view"],
"activation": ["add", "view", "change"],
"rulebook_process": ["view"],
"audit_rule": ["view"],
"organization": ["view"],
Expand All @@ -110,6 +103,7 @@
"activation": [
"add",
"view",
"change",
"enable",
"disable",
"restart",
Expand Down
24 changes: 24 additions & 0 deletions src/aap_eda/core/migrations/0055_alter_activation_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.16 on 2025-01-06 14:22

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("core", "0054_DAB_help_text"),
]

operations = [
migrations.AlterModelOptions(
name="activation",
options={
"default_permissions": ["add", "view", "change", "delete"],
"ordering": ("-created_at",),
"permissions": [
("enable_activation", "Can enable an activation"),
("disable_activation", "Can disable an activation"),
("restart_activation", "Can restart an activation"),
],
},
),
]
2 changes: 1 addition & 1 deletion src/aap_eda/core/models/activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class Meta:
("disable_activation", "Can disable an activation"),
("restart_activation", "Can restart an activation"),
]
default_permissions = ["add", "view", "delete"]
default_permissions = ["add", "view", "change", "delete"]

description = models.TextField(
default="",
Expand Down
Loading

0 comments on commit a20f967

Please sign in to comment.