diff --git a/src/api/src/backend/urls.py b/src/api/src/backend/urls.py index f52da592..2ab4fc8e 100644 --- a/src/api/src/backend/urls.py +++ b/src/api/src/backend/urls.py @@ -8,6 +8,7 @@ from backend.views.HealthCheck import HealthCheck from backend.views.Identities import Identities from backend.views.Pipelines import Pipelines +from backend.views.CIPipelines import CIPipelines from backend.views.Users import Users from backend.views.Nuke import Nuke from backend.views.ChangePipelineOwner import ChangePipelineOwner diff --git a/src/api/src/backend/views/CIPipelines.py b/src/api/src/backend/views/CIPipelines.py new file mode 100644 index 00000000..8a810501 --- /dev/null +++ b/src/api/src/backend/views/CIPipelines.py @@ -0,0 +1,130 @@ +from typing import List +from pydantic import ValidationError +from django.db import DatabaseError, IntegrityError, OperationalError +from django.forms import model_to_dict + +from backend.views.RestrictedAPIView import RestrictedAPIView +from backend.views.http.responses.errors import ( + Conflict, + BadRequest, + NotFound, + Forbidden, + ServerError as ServerErrorResp +) +from backend.views.http.responses import ResourceURLResponse +from backend.views.http.requests import CIPipeline, ImageBuildTask +from backend.models import ( + Pipeline as PipelineModel, + Archive, + PipelineArchive, + TASK_TYPE_IMAGE_BUILD +) +from backend.services.TaskService import service as task_service +from backend.services.GroupService import service as group_service +from backend.errors.api import BadRequestError +from backend.helpers import resource_url_builder + + +class CIPipelines(RestrictedAPIView): + def post(self, request, group_id, *_, **__): + """Pipeline requests with type 'ci' are supported in order to make the + process of setting up a ci/cd pipeline as simple as possible. Rather than + specifying tasks, dependencies, etc, we let user pass most the required + data in the top level of the pipeline request.""" + # 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") + + # Validate the request body based on the type of pipeline specified + prepared_request = self.prepare(CIPipeline) + + # 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 + + # Check that the id of the pipeline is unique + if PipelineModel.objects.filter(id=body.id, group=group).exists(): + return Conflict(f"A Pipeline already exists with the id '{body.id}'") + + # Create the pipeline + try: + pipeline = PipelineModel.objects.create( + id=body.id, + group=group, + owner=request.username, + max_exec_time=body.execution_profile.max_exec_time, + invocation_mode=body.execution_profile.invocation_mode, + max_retries=body.execution_profile.max_retries, + retry_policy=body.execution_profile.retry_policy, + duplicate_submission_policy=body.execution_profile.duplicate_submission_policy, + env=body.dict()["env"], + params=body.dict()["params"] + ) + except (IntegrityError, OperationalError) as e: + return BadRequest(message=e.__cause__) + except Exception as e: + return ServerErrorResp(f"{e}") + + # Fetch the archives specified in the request then create relations + # between them and the pipline + pipeline_archives = [] + try: + # Prevent duplicate pipeline archives by casting id array to 'set' + for archive_id in set(body.archive_ids): + # Fetch the archive object + archive = Archive.objects.filter(group=group, id=archive_id).first() + + # Return bad request if archive not found + if archive == None: + pipeline.delete() + return BadRequest(message=f"Archive not found with an id of '{archive_id}' and group_id '{group.id}'") + + pipeline_archives.append( + PipelineArchive.objects.create( + pipeline=pipeline, + archive=archive + ) + ) + except (IntegrityError, OperationalError, DatabaseError) as e: + # Delete the pipeline + pipeline.delete() + + # Delete the pipeline archive relationships that were just created + [pipeline_archive.delete() for pipeline_archive in pipeline_archives] + return BadRequest(message=e.__cause__) + + try: + # Build an task_request from the pipeline request body + task_request = ImageBuildTask( + id="build", + builder=body.builder, + cache=body.cache, + description="Build an image from a repository and push it to an image registry", + destination=body.destination, + context=body.context, + pipeline_id=pipeline.id, + type=TASK_TYPE_IMAGE_BUILD + ) + + # Create 'build' task + task_service.create(pipeline, task_request) + except (ValidationError, BadRequestError) as e: + pipeline.delete() + return BadRequest(message=e) + except (IntegrityError, OperationalError, DatabaseError) as e: + pipeline.delete() + return BadRequest(message=e.__cause__) + except Exception as e: + pipeline.delete() + return ServerErrorResp(message=e) + + return ResourceURLResponse( + url=resource_url_builder(request.url.replace("/ci", "/pipelines"), pipeline.id)) diff --git a/src/api/src/backend/views/Pipelines.py b/src/api/src/backend/views/Pipelines.py index 391d0661..cd7f2000 100644 --- a/src/api/src/backend/views/Pipelines.py +++ b/src/api/src/backend/views/Pipelines.py @@ -15,7 +15,7 @@ from backend.views.http.responses import BaseResponse, ResourceURLResponse from backend.views.http.requests import Pipeline, CIPipeline, ImageBuildTask from backend.models import ( - Pipeline, + Pipeline as PipelineModel, Archive, PipelineArchive, TASK_TYPE_IMAGE_BUILD @@ -52,7 +52,7 @@ def get(self, request, group_id, pipeline_id=None): return self.list(group) # Get the pipeline by the id provided in the path params - pipeline = Pipeline.objects.filter( + pipeline = PipelineModel.objects.filter( id=pipeline_id, group=group ).prefetch_related("tasks").first() @@ -71,7 +71,7 @@ def get(self, request, group_id, pipeline_id=None): return BaseResponse(result=result) def list(self, group): - pipelines = Pipeline.objects.filter(group=group) + pipelines = PipelineModel.objects.filter(group=group) return ModelListResponse(pipelines) def post(self, request, group_id, *_, **__): @@ -112,12 +112,12 @@ def post(self, request, group_id, *_, **__): body = prepared_request.body # Check that the id of the pipeline is unique - if Pipeline.objects.filter(id=body.id, group=group).exists(): + if PipelineModel.objects.filter(id=body.id, group=group).exists(): return Conflict(f"A Pipeline already exists with the id '{body.id}'") # Create the pipeline try: - pipeline = Pipeline.objects.create( + pipeline = PipelineModel.objects.create( id=body.id, group=group, owner=request.username, @@ -178,7 +178,7 @@ def delete(self, request, group_id, pipeline_id, *_, **__): return Forbidden(message="You do not have access to this group") # Get the pipeline by the id provided in the path params - pipeline = Pipeline.objects.filter( + pipeline = PipelineModel.objects.filter( id=pipeline_id, group=group ).prefetch_related("tasks").first()