diff --git a/src/api/specs/WorkflowsAPI.yaml b/src/api/specs/WorkflowsAPI.yaml index 0efda5dc..1c12f790 100644 --- a/src/api/specs/WorkflowsAPI.yaml +++ b/src/api/specs/WorkflowsAPI.yaml @@ -1553,7 +1553,43 @@ paths: application/json: schema: $ref: '#/components/schemas/RespTask' - + patch: + tags: + - Tasks + summary: Update task details + description: Update details for a task + operationId: patchTask + parameters: + - name: group_id + in: path + required: true + schema: + $ref: '#/components/schemas/ID' + - name: pipeline_id + in: path + required: true + schema: + $ref: '#/components/schemas/ID' + - name: task_id + in: path + required: true + schema: + $ref: '#/components/schemas/ID' + requestBody: + required: true + description: Request body for the pathTask operation. + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + responses: + '200': + description: Task found. + content: + application/json: + schema: + $ref: '#/components/schemas/RespTask' + delete: tags: - Tasks @@ -2222,6 +2258,26 @@ components: type: string # --- Task --- + ReqPatchTask: + anyOf: + - $ref: '#/components/schemas/ImageBuildTask' + - $ref: '#/components/schemas/RequestTask' + - $ref: '#/components/schemas/ApplicationTask' + - $ref: '#/components/schemas/TapisJobTask' + - $ref: '#/components/schemas/TapisActorTask' + - $ref: '#/components/schemas/FunctionTask' + - $ref: '#/components/schemas/TemplateTask' + discriminator: + propertyName: type + mapping: + image_build: "#/components/schemas/ImageBuildTask" + request: "#/components/schemas/RequestTask" + application: "#/components/schemas/ApplicationTask" + tapis_job: "#/components/schemas/TapisJobTask" + tapis_actor: "#/components/schemas/TapisActorTask" + function: "#/components/schemas/FunctionTask" + template: "#/components/schemas/TemplateTask" + Task: oneOf: - $ref: '#/components/schemas/ImageBuildTask' @@ -2244,9 +2300,6 @@ components: BaseTask: type: object - required: - - id - - type properties: id: type: string diff --git a/src/api/src/backend/serializers/PipelineLockModelSerializer.py b/src/api/src/backend/serializers/PipelineLockModelSerializer.py index cb06ad96..daa2926f 100644 --- a/src/api/src/backend/serializers/PipelineLockModelSerializer.py +++ b/src/api/src/backend/serializers/PipelineLockModelSerializer.py @@ -1,5 +1,3 @@ -from django.forms.models import model_to_dict - from backend.serializers import UUIDSerializer from backend.conf.constants import DATETIME_FORMAT diff --git a/src/api/src/backend/views/Tasks.py b/src/api/src/backend/views/Tasks.py index c104402b..b65f8889 100644 --- a/src/api/src/backend/views/Tasks.py +++ b/src/api/src/backend/views/Tasks.py @@ -1,6 +1,7 @@ import json -from django.db import IntegrityError, OperationalError +from django.db import IntegrityError, OperationalError, DatabaseError +from django.forms import model_to_dict from backend.models import Task, Pipeline from backend.views.RestrictedAPIView import RestrictedAPIView @@ -9,7 +10,9 @@ from backend.views.http.responses.models import ModelListResponse, ModelResponse from backend.services.TaskService import service as task_service from backend.services.GroupService import service as group_service +from backend.errors.api import ServerError as APIServerError from backend.helpers import resource_url_builder +from backend.utils import logger class Tasks(RestrictedAPIView): @@ -103,69 +106,62 @@ def post(self, request, group_id, pipeline_id, *_, **__): return ResourceURLResponse( url=resource_url_builder(request.url, task.id)) + def patch(self, request, group_id, pipeline_id, task_id): + try: + # Get the group + group = group_service.get(group_id, request.tenant_id) + if group == None: + return NotFound(f"No group found with id '{group_id}'") + + # Check that the user belongs to the group + if not group_service.user_in_group(request.username, group_id, request.tenant_id): + return Forbidden(message="You do not have access to this group") + + # Get the pipline + pipeline = Pipeline.objects.filter( + group=group, + id=pipeline_id + ).first() + + task_model = Task.objects.filter(pipeline=pipeline, id=task_id).first() + + if task_model == None: + return NotFound(f"Task with id '{task_id}' not found in pipeline '{pipeline_id}'") + + # Resolve the the proper pydantic object for this task type + TaskSchema = task_service.resolve_request_type(task_model.type) + + task = TaskSchema( + **model_to_dict(task_model), + **self.request_body + ) + + # Disallow updating the type property + if (task_model.type != task.type): + return BadRequest(f"Updating the type of a task is not allowed. Expected task.type: {task_model.type} - Recieved: {task.type}") + + # The pipeline_id property will be set to the previous value as we do not + # allow that property to be updated + Task.objects.filter( + pipeline_id=pipeline_id, + id=task_id + ).update(**{ + **json.loads(task.json()) + }) + + return ModelResponse(Task.objects.filter(id=task.id, pipeline_id=pipeline_id).first()) + except (DatabaseError, OperationalError, IntegrityError) as e: + logger.exception(e.__cause__) + return ServerError(message=e.__cause__) + except APIServerError as e: + logger.exception(e.__cause__) + return ServerError(message=e) + except Exception as e: + logger.exception(str(e)) + return ServerError(message=e) - def put(self, request, group_id, pipeline_id, task_id=None): - # Get the group - group = group_service.get(group_id, request.tenant_id) - if group == None: - return NotFound(f"No group found with id '{group_id}'") - - # Check that the user belongs to the group - if not group_service.user_in_group(request.username, group_id, request.tenant_id): - return Forbidden(message="You do not have access to this group") - - # Get the pipline - pipeline = Pipeline.objects.filter( - group=group, - id=pipeline_id - ).first() - - task = Task.objects.filter(pipeline=pipeline, id=task_id).first() - - if task == None: - return NotFound(f"Task with id '{task_id}' not found in pipeline '{pipeline_id}'") - - # Validate the request body - if "type" not in self.request_body: - return BadRequest(message="Request body missing 'type' property") - - if not task_service.is_valid_task_type(self.request_body["type"]): - return BadRequest(message=f"Invalid task type: Expected one of: {task_service.get_task_request_types()} - Recieved: {self.request_body['type']}. ") - - # Resolve the the proper request for the type of task provided in the request body - TaskRequest = task_service.resolve_request_type(self.request_body["type"]) - - # Validate and prepare the create reqeust - prepared_request = self.prepare(TaskRequest) - - # Return the failure view instance if validation failed - if not prepared_request.is_valid: - return prepared_request.failure_view - - # Get the JSON encoded body from the validation result - body = prepared_request.body - - # Disallow updating the type property - if body.type != task.type: - return BadRequest(f"Updating the type of an task is not allowed. Expected task.type: {task.type} - Recieved: {'null' if body.type == None else body.type}") - - # The pipeline_id property will be set to the previous value as we do not - # allow that property to be updated - Task.objects.filter( - pipeline_id=pipeline_id, - id=task_id - ).update(**{ - **json.loads(body.json()), - "type": task.type, - "pipeline_id": pipeline_id, - "context": task.context, - "destination": task.destination - }) - - return ModelResponse(Task.objects.filter(id=body.id, pipeline_id=pipeline_id).first()) - - def patch(self, *_, **__): - return MethodNotAllowed("Method 'PATCH' not allowed for 'Task' objects") + def put(self, *_, **__): + return MethodNotAllowed("Method 'PUT' not allowed for 'Task' objects") def delete(self, request, group_id, pipeline_id, task_id): # Get the group diff --git a/src/api/src/backend/views/UpdateTaskExecutionStatus.py b/src/api/src/backend/views/UpdateTaskExecutionStatus.py index f37cf867..55db70b3 100644 --- a/src/api/src/backend/views/UpdateTaskExecutionStatus.py +++ b/src/api/src/backend/views/UpdateTaskExecutionStatus.py @@ -35,9 +35,11 @@ def patch(self, request, task_execution_uuid, status, *_, **__): stdout=body.stdout, stderr=body.stderr ) + + return BaseResponse(result="TaskExecution status updated") + except (DatabaseError, IntegrityError, OperationalError) as e: return ServerError(f"Server Error: {e.__cause__}") except Exception as e: return ServerError(f"Server Error: {e}") - - return BaseResponse(result="TaskExecution status updated") \ No newline at end of file + \ No newline at end of file