diff --git a/README.md b/README.md index f9eb308..1cc2a2c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ POST /deploy "service": "service-name", "attributes": { "image": "hello-world" - } + }, + "forceDeploy": false } ``` diff --git a/deployer/api.py b/deployer/api.py index 937c787..a5d31c9 100644 --- a/deployer/api.py +++ b/deployer/api.py @@ -1,7 +1,7 @@ import os from flask import Blueprint, Response, current_app, request -from pydantic import BaseModel, StrictStr, ValidationError +from pydantic import BaseModel, StrictBool, StrictStr, ValidationError from werkzeug.exceptions import BadRequest from deployer.config import Config @@ -12,6 +12,9 @@ class DeployRequest(BaseModel): service: StrictStr attributes: dict[StrictStr, StrictStr] + # Normally deploys only when an attribute is changed. + # Set this to true to always do a deploy for the service. + forceDeploy: StrictBool = False def text_response(value, status): @@ -64,6 +67,7 @@ def deploy(service_locks: ServiceLocks, config: Config, deployer: Deployer): deployer.handle( service=service, attributes=model.attributes, + force_deploy=model.forceDeploy, ) return text_response("OK\n", 200) diff --git a/deployer/deployer.py b/deployer/deployer.py index 8aa4d5c..b55c388 100644 --- a/deployer/deployer.py +++ b/deployer/deployer.py @@ -81,7 +81,9 @@ def _write_deployer_file( ) -> None: deployer_json_file.write_text(json.dumps(content, indent=" ") + "\n") - def handle(self, service: ServiceModel, attributes: dict[str, str]): + def handle( + self, service: ServiceModel, attributes: dict[str, str], force_deploy: bool + ): patch_values: dict[str, str] = {} for key, value in attributes.items(): if key not in service.mappings: @@ -106,9 +108,10 @@ def handle(self, service: ServiceModel, attributes: dict[str, str]): updated_content = self._patch_content(previous_content, patch_values) if previous_content == updated_content: logger.info("No changes found") - return - - self._write_deployer_file(deployer_json_file, updated_content) + if not force_deploy: + return + else: + self._write_deployer_file(deployer_json_file, updated_content) start = time.time() @@ -120,12 +123,13 @@ def handle(self, service: ServiceModel, attributes: dict[str, str]): logger.info(f"Ansible deploy completed in {time.time() - start} s") - self._write_changes( - repo=repo, - service=service, - deployer_json_file=deployer_json_file, - patch_values=patch_values, - ) + if previous_content != updated_content: + self._write_changes( + repo=repo, + service=service, + deployer_json_file=deployer_json_file, + patch_values=patch_values, + ) finally: repo.cleanup() diff --git a/tests/test_deployer.py b/tests/test_deployer.py index 5ee5e99..2aca7c5 100644 --- a/tests/test_deployer.py +++ b/tests/test_deployer.py @@ -35,6 +35,7 @@ def test_write_retry( attributes={ "value": "hello", }, + force_deploy=False, ) assert mock_push_changes.call_count == 2 @@ -55,6 +56,54 @@ def test_ansible( attributes={ "value": "hello", }, + force_deploy=False, ) mock_push_changes.assert_called_once() + + @patch("deployer.repo.TempRepo.push_changes") + @patch("deployer.deployer.Deployer._ansible_deploy") + def test_skip_no_change( + self, + mock_ansible_deploy: MagicMock, + mock_push_changes: MagicMock, + injector: Injector, + ): + # Avoid any real side effect. + mock_ansible_deploy.return_value = None + mock_push_changes.return_value = None + + config = injector.get(Config) + deployer = Deployer(config=config) + + deployer.handle( + service=config.services["test-service1"], + attributes={}, + force_deploy=False, + ) + + mock_ansible_deploy.assert_not_called() + + @patch("deployer.repo.TempRepo.push_changes") + @patch("deployer.deployer.Deployer._ansible_deploy") + def test_force_deploy_for_no_changes( + self, + mock_ansible_deploy: MagicMock, + mock_push_changes: MagicMock, + injector: Injector, + ): + # Avoid any real side effect. + mock_ansible_deploy.return_value = None + mock_push_changes.return_value = None + + config = injector.get(Config) + deployer = Deployer(config=config) + + deployer.handle( + service=config.services["test-service1"], + attributes={}, + force_deploy=True, + ) + + mock_ansible_deploy.assert_called_once() + mock_push_changes.assert_not_called()