Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 69 additions & 18 deletions src/aap_eda/api/views/activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,30 @@ def partial_update(self, request, pk):
None,
description="The Activation has been deleted.",
),
status.HTTP_409_CONFLICT: OpenApiResponse(
None,
description="Activation blocked while Workers offline.",
),
}
| RedisDependencyMixin.redis_unavailable_response(),
parameters=[
OpenApiParameter(
name="force",
description="Force delete after worker node offline",
required=False,
type=bool,
)
],
)
def destroy(self, request, *args, **kwargs):
activation = self.get_object()
force_delete = str_to_bool(
request.query_params.get("force", "false"),
)

if activation.status == ActivationStatus.WORKERS_OFFLINE:
raise api_exc.Conflict(
f"An Activation in state '{activation.status}' cannot be "
"deleted.",
)
self._check_workers_offline_with_force(
activation, force_delete, "Deleted"
)

audit_log = logging_utils.generate_simple_audit_log(
"Delete",
Expand Down Expand Up @@ -434,14 +447,33 @@ def _start(self, request, activation: models.Activation) -> Response:
None,
description="Activation has been disabled.",
),
status.HTTP_409_CONFLICT: OpenApiResponse(
None,
description="Activation blocked while Workers offline.",
),
}
| RedisDependencyMixin.redis_unavailable_response(),
parameters=[
OpenApiParameter(
name="force",
description="Force disable after worker node offline",
required=False,
type=bool,
)
],
)
@action(methods=["post"], detail=True, rbac_action=Action.DISABLE)
def disable(self, request, pk):
activation = self.get_object()

self._check_deleting(activation)
force_disable = str_to_bool(
request.query_params.get("force", "false"),
)

self._check_workers_offline_with_force(
activation, force_disable, "Disabled"
)

if activation.is_enabled:
# Redis must be available in order to perform the delete.
Expand Down Expand Up @@ -509,19 +541,9 @@ def restart(self, request, pk):
request.query_params.get("force", "false"),
)

if (
settings.DEPLOYMENT_TYPE == "podman"
and activation.status == ActivationStatus.WORKERS_OFFLINE
and not force_restart
):
# block the restart and return an error
raise api_exc.Conflict(
"An activation with an activation_status of 'Workers offline' "
"cannot be Restarted because this will leave an orphaned "
"container running on one of the 'activation-worker-node's. "
"If you want to force a restart, please add the "
"/?force=true query param."
)
self._check_workers_offline_with_force(
activation, force_restart, "Restarted"
)
if not activation.is_enabled:
raise api_exc.Forbidden(
detail="Activation is disabled and cannot be run."
Expand Down Expand Up @@ -616,6 +638,35 @@ def _check_deleting(self, activation):
detail="Object is being deleted", code=409
)

def _check_workers_offline_with_force(
self, activation, force_flag, operation_name
):
"""
Check if activation is in WORKERS_OFFLINE status and handle force flag.

Args:
activation: The activation object to check
force_flag: Boolean indicating if force operation is requested
operation_name: String name of the operation
(e.g., "Restarted", "Disabled", "Deleted")

Raises:
api_exc.Conflict: If activation is WORKERS_OFFLINE and force flag
is False
"""
if (
settings.DEPLOYMENT_TYPE == "podman"
and activation.status == ActivationStatus.WORKERS_OFFLINE
and not force_flag
):
raise api_exc.Conflict(
f"An activation with an activation_status of "
f"'Workers offline' cannot be {operation_name} because this "
f"may leave an orphaned container running. "
f"If you want to force a {operation_name.lower()}, please "
f"add the /?force=true query param."
)


@extend_schema_view(
retrieve=extend_schema(
Expand Down
64 changes: 64 additions & 0 deletions tests/integration/api/test_activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,70 @@ def test_disable_activation_redis_unavailable(
}


@pytest.mark.django_db
@pytest.mark.parametrize(
("force_disable", "expected_response"),
[
(
"true",
status.HTTP_204_NO_CONTENT,
),
(
"false",
status.HTTP_409_CONFLICT,
),
],
)
@patch("aap_eda.api.serializers.activation.settings.DEPLOYMENT_TYPE", "podman")
def test_disable_activation_workers_offline(
force_disable,
expected_response,
default_activation: models.Activation,
admin_client: APIClient,
preseed_credential_types,
):
default_activation.status = enums.ActivationStatus.WORKERS_OFFLINE
default_activation.save(update_fields=["status"])

response = admin_client.post(
f"{api_url_v1}/activations/{default_activation.id}/disable/"
f"?force={force_disable}"
)
assert response.status_code == expected_response


@pytest.mark.django_db
@pytest.mark.parametrize(
("force_delete", "expected_response"),
[
(
"true",
status.HTTP_204_NO_CONTENT,
),
(
"false",
status.HTTP_409_CONFLICT,
),
],
)
@patch("aap_eda.api.serializers.activation.settings.DEPLOYMENT_TYPE", "podman")
def test_delete_activation_workers_offline(
force_delete,
expected_response,
default_activation: models.Activation,
admin_client: APIClient,
preseed_credential_types,
):
default_activation.status = enums.ActivationStatus.WORKERS_OFFLINE
default_activation.save(update_fields=["status"])

response = admin_client.delete(
f"{api_url_v1}/activations/{default_activation.id}/"
f"?force={force_delete}"
)
assert response.status_code == expected_response


@pytest.mark.django_db
def test_list_activation_instances(
default_activation: models.Activation,
Expand Down