diff --git a/Dockerfile b/Dockerfile index 20c554c..d16d6f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM cloudblueconnect/connect-extension-runner:28.8 +FROM cloudblueconnect/connect-extension-runner:28.9 COPY pyproject.toml /install_temp/. COPY poetry.* /install_temp/. diff --git a/connect_ext_ppr/tasks_manager.py b/connect_ext_ppr/tasks_manager.py index eebd0a7..714d688 100644 --- a/connect_ext_ppr/tasks_manager.py +++ b/connect_ext_ppr/tasks_manager.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import joinedload, selectinload from connect_ext_ppr.client.exception import ClientError -from connect_ext_ppr.db import get_db_ctx_manager +from connect_ext_ppr.db import get_cbc_extension_db, get_db_ctx_manager from connect_ext_ppr.models.enums import CBCTaskLogStatus from connect_ext_ppr.models.enums import ( DeploymentRequestStatusChoices, @@ -16,12 +16,23 @@ from connect_ext_ppr.models.deployment import DeploymentRequest from connect_ext_ppr.models.ppr import PPRVersion from connect_ext_ppr.models.task import Task +from connect_ext_ppr.services.cbc_extension import get_hub_credentials +from connect_ext_ppr.services.cbc_hub import CBCService class TaskException(Exception): pass +def _get_cbc_service(config, deployment): + cbc_db = get_cbc_extension_db(config) + hub_credentials = get_hub_credentials(deployment.hub_id, cbc_db) + if not hub_credentials: + raise TaskException(f'Hub Credentials not found for Hub ID {deployment.hub_id}') + + return CBCService(hub_credentials, False) + + def _execute_with_retries(function, func_args, num_retries=5): """ @param function: reference to function to execute @@ -71,15 +82,38 @@ def _check_cbc_task_status(cbc_service, tracking_id): raise TaskException(f'Something went wrong with task: {tracking_id}') -def validate_ppr(deployment_request): +def validate_ppr(deployment_request, **kwargs): return True -def apply_ppr_and_delegate_to_marketplaces(deployment_request): +def check_and_update_product(deployment_request, cbc_service, **kwargs): + if not deployment_request.manually: + + product_id = deployment_request.deployment.product_id + + response = _execute_with_retries( + cbc_service.get_product_details, func_args={'product_id': product_id}, + ) + + if 'error' in response.keys(): + raise Exception(response['error']) + + if response.get('isUpdateAvailable'): + response = _execute_with_retries( + cbc_service.update_product, func_args={'product_id': product_id}, + ) + + if 'error' in response.keys(): + raise Exception(response['error']) + + return True + + +def apply_ppr_and_delegate_to_marketplaces(deployment_request, **kwargs): return True -def delegate_to_l2(deployment_request): +def delegate_to_l2(deployment_request, **kwargs): return True @@ -90,8 +124,10 @@ def delegate_to_l2(deployment_request): } -def execute_tasks(db, tasks): +def execute_tasks(db, config, tasks): was_succesfull = False + cbc_service = _get_cbc_service(config, tasks[0].deployment_request.deployment) + for task in tasks: db.refresh(task, with_for_update=True) if task.status == TasksStatusChoices.pending: @@ -101,7 +137,10 @@ def execute_tasks(db, tasks): db.commit() try: - was_succesfull = TASK_PER_TYPE.get(task.type)(task.deployment_request) + was_succesfull = TASK_PER_TYPE.get(task.type)( + deployment_request=task.deployment_request, + cbc_service=cbc_service, + ) task.status = TasksStatusChoices.done if not was_succesfull: task.status = TasksStatusChoices.error @@ -148,7 +187,7 @@ def main_process(deployment_request_id, config): deployment_request_id=deployment_request_id, ).order_by(Task.id).all() - was_succesfull = execute_tasks(db, tasks) + was_succesfull = execute_tasks(db, config, tasks) db.refresh(deployment_request, with_for_update=True) diff --git a/tests/conftest.py b/tests/conftest.py index bcbb160..f64723f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -176,6 +176,7 @@ def _build_deployment_request( status=None, started_at=None, finished_at=None, + manually=False, ): if not deployment: deployment = deployment_factory() @@ -185,8 +186,10 @@ def _build_deployment_request( dep_req = DeploymentRequest( deployment_id=deployment.id, + deployment=deployment, ppr_id=ppr.id, created_by=deployment.account_id, + manually=manually, delegate_l2=delegate_l2, status=status, started_at=started_at, @@ -194,6 +197,7 @@ def _build_deployment_request( ) dbsession.add(ppr) dbsession.set_next_verbose(dep_req, 'deployment_id') + dbsession.commit() return dep_req return _build_deployment_request diff --git a/tests/test_tasks_manager.py b/tests/test_tasks_manager.py index 18fea29..05e2b8d 100644 --- a/tests/test_tasks_manager.py +++ b/tests/test_tasks_manager.py @@ -1,6 +1,9 @@ import copy import pytest + +from unittest.mock import patch + from sqlalchemy import null from connect_ext_ppr.models.deployment import Deployment, DeploymentRequest @@ -17,11 +20,13 @@ _check_cbc_task_status, _send_ppr, apply_ppr_and_delegate_to_marketplaces, + check_and_update_product, delegate_to_l2, main_process, TaskException, validate_ppr, ) +from connect_ext_ppr.services.cbc_hub import CBCService def test_apply_ppr_and_delegate_to_marketplaces(deployment_request_factory): @@ -74,12 +79,127 @@ def test__check_cbc_task_status_with_max_retries(task_logs_response, mocker): assert str(ex) == 'Some random error' +@patch.object(CBCService, 'update_product') +@patch.object(CBCService, 'get_product_details') +@patch.object(CBCService, '__init__') +def test_check_and_update_product( + mock___init__, + mock_get_product_details, + mock_update_product, + product_details, + update_product_response, + deployment_request_factory, +): + + mock___init__.return_value = None + product_details['isUpdateAvailable'] = True + mock_get_product_details.return_value = product_details + mock_update_product.return_value = update_product_response + + assert check_and_update_product( + deployment_request=deployment_request_factory(), cbc_service=CBCService(), + ) + assert mock_get_product_details.call_count == 1 + assert mock_update_product.call_count == 1 + + +@patch.object(CBCService, 'update_product') +@patch.object(CBCService, 'get_product_details') +@patch.object(CBCService, '__init__') +def test_check_and_update_product_no_update_needed( + mock___init__, + mock_get_product_details, + mock_update_product, + product_details, + update_product_response, + deployment_request_factory, +): + + mock___init__.return_value = None + product_details['isUpdateAvailable'] = False + mock_get_product_details.return_value = product_details + mock_update_product.return_value = update_product_response + + assert check_and_update_product( + deployment_request=deployment_request_factory(), cbc_service=CBCService(), + ) + assert mock_get_product_details.call_count == 1 + assert mock_update_product.call_count == 0 + + +@patch.object(CBCService, 'update_product') +@patch.object(CBCService, 'get_product_details') +@patch.object(CBCService, '__init__') +def test_check_and_update_product_manually( + mock___init__, + mock_get_product_details, + mock_update_product, + product_details, + update_product_response, + deployment_request_factory, +): + + mock___init__.return_value = None + product_details['isUpdateAvailable'] = True + mock_get_product_details.return_value = product_details + mock_update_product.return_value = update_product_response + + assert check_and_update_product( + deployment_request=deployment_request_factory(manually=True), cbc_service=CBCService(), + ) + + assert mock_get_product_details.call_count == 0 + assert mock_update_product.call_count == 0 + + +@patch.object(CBCService, 'get_product_details') +@patch.object(CBCService, '__init__') +def test_check_and_update_product_w_errors_in_get_details( + mock___init__, + mock_get_product_details, + get_product_details_not_found_response, + deployment_request_factory, +): + mock___init__.return_value = None + mock_get_product_details.return_value = get_product_details_not_found_response + + with pytest.raises(Exception): + check_and_update_product( + deployment_request=deployment_request_factory(), cbc_service=CBCService(), + ) + + +@patch.object(CBCService, 'update_product') +@patch.object(CBCService, 'get_product_details') +@patch.object(CBCService, '__init__') +def test_check_and_update_product_w_errors_in_update_product( + mock___init__, + mock_get_product_details, + mock_update_product, + product_details, + product_not_installed_response, + deployment_request_factory, +): + mock___init__.return_value = None + product_details['isUpdateAvailable'] = True + mock_get_product_details.return_value = product_details + mock_update_product.return_value = product_not_installed_response + + with pytest.raises(Exception): + check_and_update_product( + deployment_request=deployment_request_factory(), cbc_service=CBCService(), + ) + + +@patch.object(CBCService, '__init__', return_value=None) def test_main_process( + mock___init__, dbsession, deployment_factory, deployment_request_factory, task_factory, ppr_version_factory, + mocker, ): dep = deployment_factory() ppr = ppr_version_factory(id='PPR-123', product_version=1, deployment=dep) @@ -87,7 +207,11 @@ def test_main_process( task_factory(deployment_request=dr, task_index='0001', type=TaskTypesChoices.ppr_validation) task_factory(deployment_request=dr, task_index='0002', type=TaskTypesChoices.apply_and_delegate) task_factory(deployment_request=dr, task_index='0003', type=TaskTypesChoices.delegate_to_l2) - assert main_process(dr.id, {}) == DeploymentRequestStatusChoices.done + + with mocker.patch( + 'connect_ext_ppr.tasks_manager._get_cbc_service', return_value=CBCService(), + ): + assert main_process(dr.id, {}) == DeploymentRequestStatusChoices.done assert dbsession.query(Deployment).filter_by(status=DeploymentStatusChoices.synced).count() == 1 assert dbsession.query(DeploymentRequest).filter_by( @@ -100,18 +224,23 @@ def test_main_process( ).count() == 3 +@patch.object(CBCService, '__init__', return_value=None) def test_main_process_wo_l2_delegation( + _, dbsession, deployment_factory, deployment_request_factory, task_factory, ppr_version_factory, + mocker, ): dep = deployment_factory() ppr = ppr_version_factory(id='PPR-123', product_version=1, deployment=dep) dr = deployment_request_factory(deployment=dep, delegate_l2=False, ppr=ppr) task_factory(deployment_request=dr, task_index='0001', type=TaskTypesChoices.ppr_validation) task_factory(deployment_request=dr, task_index='0002', type=TaskTypesChoices.apply_and_delegate) + + mocker.patch('connect_ext_ppr.tasks_manager._get_cbc_service', return_value=CBCService()) assert main_process(dr.id, {}) == DeploymentRequestStatusChoices.done assert dbsession.query(Deployment).filter_by( @@ -127,13 +256,16 @@ def test_main_process_wo_l2_delegation( ).count() == 2 +@patch.object(CBCService, '__init__', return_value=None) def test_main_process_deployment_w_new_ppr_version( + _, dbsession, file_factory, deployment_factory, deployment_request_factory, task_factory, ppr_version_factory, + mocker, ): ppr_file = file_factory(id='MFL-123') dep = deployment_factory() @@ -144,6 +276,8 @@ def test_main_process_deployment_w_new_ppr_version( task_factory(deployment_request=dr, task_index='0001', type=TaskTypesChoices.ppr_validation) task_factory(deployment_request=dr, task_index='0002', type=TaskTypesChoices.apply_and_delegate) task_factory(deployment_request=dr, task_index='0003', type=TaskTypesChoices.delegate_to_l2) + + mocker.patch('connect_ext_ppr.tasks_manager._get_cbc_service', return_value=CBCService()) assert main_process(dr.id, {}) == DeploymentRequestStatusChoices.done assert dbsession.query(Deployment).filter_by( @@ -159,6 +293,7 @@ def test_main_process_deployment_w_new_ppr_version( ).count() == 3 +@patch.object(CBCService, '__init__', return_value=None) @pytest.mark.parametrize( ('type_function_to_mock', 'done_tasks', 'tasks_w_errors', 'pending_tasks'), ( @@ -168,6 +303,7 @@ def test_main_process_deployment_w_new_ppr_version( ), ) def test_main_process_ends_w_error( + _, dbsession, deployment_factory, deployment_request_factory, @@ -189,9 +325,11 @@ def test_main_process_ends_w_error( my_mock = mocker.Mock() def mock_get(key): - return lambda dr: key != type_function_to_mock + print(type_function_to_mock) + return lambda **kwargs: key != type_function_to_mock my_mock.get = mock_get + mocker.patch('connect_ext_ppr.tasks_manager._get_cbc_service', return_value=CBCService()) mocker.patch('connect_ext_ppr.tasks_manager.TASK_PER_TYPE', my_mock) assert main_process(dr.id, {}) == DeploymentRequestStatusChoices.error @@ -219,6 +357,7 @@ def mock_get(key): ).count() == pending_tasks +@patch.object(CBCService, '__init__', return_value=None) @pytest.mark.parametrize( ('task_statuses', 'done_tasks', 'aborted_tasks'), ( @@ -240,6 +379,7 @@ def mock_get(key): ), ) def test_main_process_w_aborted_tasks( + _, dbsession, deployment_factory, deployment_request_factory, @@ -248,6 +388,7 @@ def test_main_process_w_aborted_tasks( task_statuses, done_tasks, aborted_tasks, + mocker, ): """ We only process DeploymentRequest that are in Pending status. So in this case we asume that @@ -287,6 +428,7 @@ def change_dr_status(instance, attribute_names=None, with_for_update=None): return instance dbsession.refresh = change_dr_status + mocker.patch('connect_ext_ppr.tasks_manager._get_cbc_service', return_value=CBCService()) assert main_process(dr.id, {}) == DeploymentRequestStatusChoices.aborted @@ -305,7 +447,9 @@ def change_dr_status(instance, attribute_names=None, with_for_update=None): ).count() == aborted_tasks +@patch.object(CBCService, '__init__', return_value=None) def test_main_process_w_aborted_deployment_request( + _, dbsession, deployment_factory, deployment_request_factory, @@ -358,6 +502,7 @@ def test_main_process_w_aborted_deployment_request( ).count() == 3 +@patch.object(CBCService, '__init__', return_value=None) @pytest.mark.parametrize( ('type_function_to_mock', 'done_tasks', 'tasks_w_errors', 'pending_tasks'), ( @@ -367,6 +512,7 @@ def test_main_process_w_aborted_deployment_request( ), ) def test_main_process_ends_w_task_exception( + _, dbsession, deployment_factory, deployment_request_factory, @@ -390,11 +536,12 @@ def test_main_process_ends_w_task_exception( def mock_get(key): if key == type_function_to_mock: raise Exception('Unexpected Error') - return lambda dr: True + return lambda **kwargs: True my_mock.get = mock_get mocker.patch('connect_ext_ppr.tasks_manager.TASK_PER_TYPE', my_mock) + mocker.patch('connect_ext_ppr.tasks_manager._get_cbc_service', return_value=CBCService()) assert main_process(dr.id, {}) == DeploymentRequestStatusChoices.error assert dbsession.query(Deployment).filter_by(