Skip to content

Commit dbda0f2

Browse files
fix: handle force flag for activation in workers-offline
Signed-off-by: Alex <[email protected]>
1 parent 7813e2f commit dbda0f2

File tree

2 files changed

+132
-19
lines changed

2 files changed

+132
-19
lines changed

src/aap_eda/api/views/activation.py

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import redis
1717
from ansible_base.rbac.api.related import check_related_permissions
1818
from ansible_base.rbac.models import RoleDefinition
19-
from django.conf import settings
2019
from django.db import transaction
2120
from django.forms import model_to_dict
2221
from django_filters import rest_framework as defaultfilters
@@ -208,17 +207,30 @@ def partial_update(self, request, pk):
208207
None,
209208
description="The Activation has been deleted.",
210209
),
210+
status.HTTP_409_CONFLICT: OpenApiResponse(
211+
None,
212+
description="Activation blocked while Workers offline.",
213+
),
211214
}
212215
| RedisDependencyMixin.redis_unavailable_response(),
216+
parameters=[
217+
OpenApiParameter(
218+
name="force",
219+
description="Force delete after worker node offline",
220+
required=False,
221+
type=bool,
222+
)
223+
],
213224
)
214225
def destroy(self, request, *args, **kwargs):
215226
activation = self.get_object()
227+
force_delete = str_to_bool(
228+
request.query_params.get("force", "false"),
229+
)
216230

217-
if activation.status == ActivationStatus.WORKERS_OFFLINE:
218-
raise api_exc.Conflict(
219-
f"An Activation in state '{activation.status}' cannot be "
220-
"deleted.",
221-
)
231+
self._check_workers_offline_with_force(
232+
activation, force_delete, "Deleted"
233+
)
222234

223235
audit_log = logging_utils.generate_simple_audit_log(
224236
"Delete",
@@ -434,14 +446,33 @@ def _start(self, request, activation: models.Activation) -> Response:
434446
None,
435447
description="Activation has been disabled.",
436448
),
449+
status.HTTP_409_CONFLICT: OpenApiResponse(
450+
None,
451+
description="Activation blocked while Workers offline.",
452+
),
437453
}
438454
| RedisDependencyMixin.redis_unavailable_response(),
455+
parameters=[
456+
OpenApiParameter(
457+
name="force",
458+
description="Force disable after worker node offline",
459+
required=False,
460+
type=bool,
461+
)
462+
],
439463
)
440464
@action(methods=["post"], detail=True, rbac_action=Action.DISABLE)
441465
def disable(self, request, pk):
442466
activation = self.get_object()
443467

444468
self._check_deleting(activation)
469+
force_disable = str_to_bool(
470+
request.query_params.get("force", "false"),
471+
)
472+
473+
self._check_workers_offline_with_force(
474+
activation, force_disable, "Disabled"
475+
)
445476

446477
if activation.is_enabled:
447478
# Redis must be available in order to perform the delete.
@@ -509,19 +540,9 @@ def restart(self, request, pk):
509540
request.query_params.get("force", "false"),
510541
)
511542

512-
if (
513-
settings.DEPLOYMENT_TYPE == "podman"
514-
and activation.status == ActivationStatus.WORKERS_OFFLINE
515-
and not force_restart
516-
):
517-
# block the restart and return an error
518-
raise api_exc.Conflict(
519-
"An activation with an activation_status of 'Workers offline' "
520-
"cannot be Restarted because this will leave an orphaned "
521-
"container running on one of the 'activation-worker-node's. "
522-
"If you want to force a restart, please add the "
523-
"/?force=true query param."
524-
)
543+
self._check_workers_offline_with_force(
544+
activation, force_restart, "Restarted"
545+
)
525546
if not activation.is_enabled:
526547
raise api_exc.Forbidden(
527548
detail="Activation is disabled and cannot be run."
@@ -616,6 +637,34 @@ def _check_deleting(self, activation):
616637
detail="Object is being deleted", code=409
617638
)
618639

640+
def _check_workers_offline_with_force(
641+
self, activation, force_flag, operation_name
642+
):
643+
"""
644+
Check if activation is in WORKERS_OFFLINE status and handle force flag.
645+
646+
Args:
647+
activation: The activation object to check
648+
force_flag: Boolean indicating if force operation is requested
649+
operation_name: String name of the operation
650+
(e.g., "Restarted", "Disabled", "Deleted")
651+
652+
Raises:
653+
api_exc.Conflict: If activation is WORKERS_OFFLINE and force flag
654+
is False
655+
"""
656+
if (
657+
activation.status == ActivationStatus.WORKERS_OFFLINE
658+
and not force_flag
659+
):
660+
raise api_exc.Conflict(
661+
f"An activation with an activation_status of "
662+
f"'Workers offline' cannot be {operation_name} because this "
663+
f"may leave an orphaned container running. "
664+
f"If you want to force a {operation_name.lower()}, please "
665+
f"add the /?force=true query param."
666+
)
667+
619668

620669
@extend_schema_view(
621670
retrieve=extend_schema(

tests/integration/api/test_activation.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,70 @@ def test_disable_activation_redis_unavailable(
836836
}
837837

838838

839+
@pytest.mark.django_db
840+
@pytest.mark.parametrize(
841+
("force_disable", "expected_response"),
842+
[
843+
(
844+
"true",
845+
status.HTTP_204_NO_CONTENT,
846+
),
847+
(
848+
"false",
849+
status.HTTP_409_CONFLICT,
850+
),
851+
],
852+
)
853+
@patch("aap_eda.api.serializers.activation.settings.DEPLOYMENT_TYPE", "podman")
854+
def test_disable_activation_workers_offline(
855+
force_disable,
856+
expected_response,
857+
default_activation: models.Activation,
858+
admin_client: APIClient,
859+
preseed_credential_types,
860+
):
861+
default_activation.status = enums.ActivationStatus.WORKERS_OFFLINE
862+
default_activation.save(update_fields=["status"])
863+
864+
response = admin_client.post(
865+
f"{api_url_v1}/activations/{default_activation.id}/disable/"
866+
f"?force={force_disable}"
867+
)
868+
assert response.status_code == expected_response
869+
870+
871+
@pytest.mark.django_db
872+
@pytest.mark.parametrize(
873+
("force_delete", "expected_response"),
874+
[
875+
(
876+
"true",
877+
status.HTTP_204_NO_CONTENT,
878+
),
879+
(
880+
"false",
881+
status.HTTP_409_CONFLICT,
882+
),
883+
],
884+
)
885+
@patch("aap_eda.api.serializers.activation.settings.DEPLOYMENT_TYPE", "podman")
886+
def test_delete_activation_workers_offline(
887+
force_delete,
888+
expected_response,
889+
default_activation: models.Activation,
890+
admin_client: APIClient,
891+
preseed_credential_types,
892+
):
893+
default_activation.status = enums.ActivationStatus.WORKERS_OFFLINE
894+
default_activation.save(update_fields=["status"])
895+
896+
response = admin_client.delete(
897+
f"{api_url_v1}/activations/{default_activation.id}/"
898+
f"?force={force_delete}"
899+
)
900+
assert response.status_code == expected_response
901+
902+
839903
@pytest.mark.django_db
840904
def test_list_activation_instances(
841905
default_activation: models.Activation,

0 commit comments

Comments
 (0)