async def save_and_deploy_resource( resource: Resource, resource_repo: ResourceRepository, operations_repo: OperationRepository, resource_template_repo: ResourceTemplateRepository, user: User, resource_template: ResourceTemplate) -> Operation: try: resource.user = user resource.updatedWhen = get_timestamp() resource_repo.save_item(resource) except Exception as e: logging.error(f'Failed saving resource item {resource}: {e}') raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=strings.STATE_STORE_ENDPOINT_NOT_RESPONDING) try: operation = await send_resource_request_message( resource=resource, operations_repo=operations_repo, resource_repo=resource_repo, user=user, resource_template=resource_template, resource_template_repo=resource_template_repo, action=RequestAction.Install) return operation except Exception as e: resource_repo.delete_item(resource.id) logging.error(f"Failed send resource request message: {e}") raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=strings.SERVICE_BUS_GENERAL_ERROR_MESSAGE)
def try_upgrade(resource_repo: ResourceRepository, resource_template_repo: ResourceTemplateRepository, properties: dict, user: User, resource_to_update_id: str) -> Resource: resource_to_update = resource_repo.get_resource_by_id(resource_to_update_id) # get the template for the resource to upgrade parent_service_name = "" if resource_to_update.resourceType == ResourceType.UserResource: parent_service_name = resource_to_update["parentWorkspaceServiceId"] resource_template_to_send = resource_template_repo.get_current_template(resource_to_update.templateName, resource_to_update.resourceType, parent_service_name) # create the patch patch = ResourcePatch( properties=properties ) # validate and submit the patch resource_to_send, _ = resource_repo.patch_resource( resource=resource_to_update, resource_patch=patch, resource_template=resource_template_to_send, etag=resource_to_update.etag, resource_template_repo=resource_template_repo, user=user) return resource_to_send
def build_step_list(self, steps: List[OperationStep], resource_template_dict: dict, action: str, resource_repo: ResourceRepository, resource_id: str, status: Status, message: str): if "pipeline" in resource_template_dict and resource_template_dict[ "pipeline"] is not None: if action in resource_template_dict[ "pipeline"] and resource_template_dict["pipeline"][ action] is not None: for step in resource_template_dict["pipeline"][action]: if step["stepId"] == "main": steps.append( self.create_main_step( resource_template=resource_template_dict, action=action, resource_id=resource_id, status=status, message=message)) else: resource_for_step = resource_repo.get_resource_by_template_name( step["resourceTemplateName"]) steps.append( OperationStep( stepId=step["stepId"], stepTitle=step["stepTitle"], resourceId=resource_for_step.id, resourceTemplateName=step[ "resourceTemplateName"], resourceType=step["resourceType"], resourceAction=step["resourceAction"], updatedWhen=self.get_timestamp())) return steps
def update_resource_for_step(operation_step: OperationStep, resource_repo: ResourceRepository, resource_template_repo: ResourceTemplateRepository, primary_resource_id: str, resource_to_update_id: str, primary_action: str, user: User) -> Resource: # create properties dict - for now we create a basic, string only dict to use as a patch # get primary resource to use in substitutions primary_resource = resource_repo.get_resource_by_id(primary_resource_id) # if this is main, just leave it alone and return it if operation_step.stepId == "main": return primary_resource # get the template for the primary resource, to get all the step details for substitutions primary_parent_service_name = "" if primary_resource.resourceType == ResourceType.UserResource: primary_parent_workspace_service = resource_repo.get_resource_by_id(primary_resource.parentWorkspaceServiceId) primary_parent_service_name = primary_parent_workspace_service.templateName primary_template = resource_template_repo.get_current_template(primary_resource.templateName, primary_resource.resourceType, primary_parent_service_name) # get the template step template_step = None for step in primary_template.pipeline.dict()[primary_action]: if step["stepId"] == operation_step.stepId: template_step = parse_obj_as(PipelineStep, step) break if template_step is None: raise f"Cannot find step with id of {operation_step.stepId} in template {primary_resource.templateName} for action {primary_action}" # TODO: actual substitution logic #1679 properties = {} for prop in template_step.properties: properties[prop.name] = prop.value if template_step.resourceAction == "upgrade": resource_to_send = try_upgrade_with_retries( num_retries=3, attempt_count=0, resource_repo=resource_repo, resource_template_repo=resource_template_repo, properties=properties, user=user, resource_to_update_id=resource_to_update_id ) return resource_to_send else: raise Exception("Only upgrade is currently supported for pipeline steps")
async def receive_message_and_update_deployment(app) -> None: """ Receives messages from the deployment status update queue and updates the status for the associated resource in the state store. Args: app ([FastAPI]): Handle to the currently running app """ receive_message_gen = receive_message() try: async for message in receive_message_gen: operations_repo = OperationRepository(get_db_client(app)) resource_repo = ResourceRepository(get_db_client(app)) resource_template_repo = ResourceTemplateRepository( get_db_client(app)) result = await update_status_in_database(resource_repo, operations_repo, resource_template_repo, message) await receive_message_gen.asend(result) except StopAsyncIteration: # the async generator when finished signals end with this exception. pass
def resource_repo(): with patch('azure.cosmos.CosmosClient') as cosmos_client_mock: yield ResourceRepository(cosmos_client_mock)
async def update_status_in_database( resource_repo: ResourceRepository, operations_repo: OperationRepository, resource_template_repo: ResourceTemplateRepository, message: DeploymentStatusUpdateMessage): """ Get the operation the message references, and find the step within the operation that is to be updated Update the status of the step. If it's a single step operation, copy the status into the operation status. If it's a multi step, update the step and set the overall status to "pipeline_deploying". If there is another step in the operation after this one, process the substitutions + patch, then enqueue a message to process it. """ result = False try: # update the op operation = operations_repo.get_operation_by_id( str(message.operationId)) step_to_update = None is_last_step = False current_step_index = 0 for i, step in enumerate(operation.steps): if step.stepId == message.stepId: step_to_update = step current_step_index = i if i == (len(operation.steps) - 1): is_last_step = True if step_to_update is None: raise f"Error finding step {message.stepId} in operation {message.operationId}" # update the step status update_step_status(step_to_update, message) # update the overall headline operation status update_overall_operation_status(operation, step_to_update, is_last_step) # save the operation operations_repo.update_item(operation) # if the step failed, or this queue message is an intermediary ("now deploying..."), return here. if not step_to_update.is_success(): return True # update the resource doc to persist any outputs resource = resource_repo.get_resource_dict_by_id( uuid.UUID(step_to_update.resourceId)) resource_to_persist = create_updated_resource_document( resource, message) resource_repo.update_item_dict(resource_to_persist) # more steps in the op to do? if is_last_step is False: assert current_step_index < (len(operation.steps) - 1) next_step = operation.steps[current_step_index + 1] resource_to_send = update_resource_for_step( operation_step=next_step, resource_repo=resource_repo, resource_template_repo=resource_template_repo, primary_resource_id=operation.resourceId, resource_to_update_id=next_step.resourceId, primary_action=operation.action, user=operation.user) # create + send the message logging.info( f"Sending next step in operation to deployment queue -> step_id: {next_step.stepId}, action: {next_step.resourceAction}" ) content = json.dumps( resource_to_send.get_resource_request_message_payload( operation_id=operation.id, step_id=next_step.stepId, action=next_step.resourceAction)) await send_deployment_message(content=content, correlation_id=operation.id, session_id=resource_to_send.id, action=next_step.resourceAction) result = True except EntityDoesNotExist: # Marking as true as this message will never succeed anyways and should be removed from the queue. result = True error_string = strings.DEPLOYMENT_STATUS_ID_NOT_FOUND.format( message.id) logging.error(error_string) except Exception as e: logging.error(strings.STATE_STORE_ENDPOINT_NOT_RESPONDING + " " + str(e)) return result
def resource_repo() -> ResourceRepository: with patch("azure.cosmos.CosmosClient") as cosmos_client_mock: return ResourceRepository(cosmos_client_mock)