diff --git a/requirements.txt b/requirements.txt index bfdb70c..bcf54e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -pyyaml -flask -pyjwt -Flask-HTTPAuth -python-dotenv -requests +pyyaml==6.0.1 +flask==3.0.0 +pyjwt==2.8.0 +Flask-HTTPAuth==4.8.0 +python-dotenv==1.0.0 +requests==2.31.0 +filelock==3.13.1 \ No newline at end of file diff --git a/server/deployer.py b/server/deployer.py index 5358387..a15ccaf 100644 --- a/server/deployer.py +++ b/server/deployer.py @@ -4,6 +4,8 @@ import subprocess import typing +import filelock + from .utils import ComposeHelper, DeploymentConfig, NginxHelper, SecretsHelper logger = logging.getLogger(__name__) @@ -16,26 +18,32 @@ def __init__(self, config: DeploymentConfig): "DEPLOYMENTS_MOUNT_DIR" ) self._deployment_namespace = f"{self._config.project_name}_{self._config.branch_name}_{config.get_project_hash()}" + self._lock_file_path = os.path.join( + os.environ.get("LOCK_FILE_BASE_PATH") or "/tmp", + f"{self._deployment_namespace}.lock", + ) + self._lock = filelock.FileLock(self._lock_file_path) self._project_path: typing.Final[str] = os.path.join( self._DEPLOYMENTS_MOUNT_DIR, self._deployment_namespace ) - if config.rest_action != "DELETE": - self._setup_project() + with self._lock: + if config.rest_action != "DELETE": + self._setup_project() - self._compose_helper = ComposeHelper( - os.path.join(self._project_path, config.compose_file_location), - config.rest_action != "DELETE", - ) - self._secrets_helper = SecretsHelper( - self._config.project_name, self._config.branch_name, self._project_path - ) - self._outer_proxy_conf_location = ( - os.environ.get("NGINX_PROXY_CONF_LOCATION") or "/etc/nginx/conf.d" - ) - self._nginx_helper = NginxHelper( - config, self._outer_proxy_conf_location, self._project_path - ) + self._compose_helper = ComposeHelper( + os.path.join(self._project_path, config.compose_file_location), + config.rest_action != "DELETE", + ) + self._secrets_helper = SecretsHelper( + self._config.project_name, self._config.branch_name, self._project_path + ) + self._outer_proxy_conf_location = ( + os.environ.get("NGINX_PROXY_CONF_LOCATION") or "/etc/nginx/conf.d" + ) + self._nginx_helper = NginxHelper( + config, self._outer_proxy_conf_location, self._project_path + ) def _clone_project(self): process = subprocess.Popen( @@ -50,18 +58,18 @@ def _clone_project(self): stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) - - stdout, stderr = process.communicate() - + try: + stdout, stderr = process.communicate() + except subprocess.TimeoutExpired as e: + logger.error(f"Error cloning the repo {self._config} with {e}") + raise if process.returncode == 0: logger.info("Git clone successful.") else: logger.error(f"Git clone failed. Return code: {process.returncode}") - logger.error("Standard Output:") - logger.error(stdout.decode()) - logger.error("Standard Error:") - logger.error(stderr.decode()) - raise Exception("Git clone failed") + logger.error(f"Standard Output: {stdout.decode()}") + logger.error(f"Standard Error: {stderr.decode()}") + raise Exception(f"Cloning the Git repo failed {self._config}") def _setup_project(self): if os.path.exists(self._project_path): @@ -89,11 +97,6 @@ def _deploy_project(self): ) return urls - def deploy_preview_environment(self): - urls = self._deploy_project() - self._configure_outer_proxy() - return urls - def _delete_deployment_files(self): if not os.path.exists(self._project_path): print(f"{self._project_path} already deleted!") @@ -103,8 +106,15 @@ def _delete_deployment_files(self): except Exception as e: logger.debug(f"Error removing deployment files {e}") + def deploy_preview_environment(self): + with self._lock: + urls = self._deploy_project() + self._configure_outer_proxy() + return urls + def delete_preview_environment(self): - self._compose_helper.remove_services() - self._nginx_helper.remove_outer_proxy() - self._nginx_helper.reload_nginx() - self._delete_deployment_files() + with self._lock: + self._compose_helper.remove_services() + self._nginx_helper.remove_outer_proxy() + self._nginx_helper.reload_nginx() + self._delete_deployment_files() diff --git a/server/utils.py b/server/utils.py index 4a8498c..ab3a6e2 100644 --- a/server/utils.py +++ b/server/utils.py @@ -26,6 +26,12 @@ class DeploymentConfig: def get_project_hash(self): return get_random_stub(f"{self.project_name}:{self.branch_name}") + def __repr__(self): + return ( + f"DeploymentConfig({self.project_name!r}, {self.branch_name!r}, {self.project_git_url!r}, " + f"{self.compose_file_location!r}, {self.rest_action!r})" + ) + class ComposeHelper: NGINX_SERVICE_TEMPLATE: typing.Final[ diff --git a/tests/conftest.py b/tests/conftest.py index f2fa7ac..09be9c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -77,6 +77,7 @@ def deployment_config(): project_name="test-project-name", branch_name="test-branch-name", project_git_url="https://github.com/tushar5526/test-project-name.git", + rest_action="POST", ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4a84e0d..4eac20f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -236,3 +236,11 @@ def test_remove_outer_proxy_when_file_is_deleted_already(nginx_helper, mocker): nginx_helper.remove_outer_proxy() # Then No error should be raised + + +def test_deployment_config_repr(deployment_config): + expected_repr = ( + "DeploymentConfig('test-project-name', 'test-branch-name', " + "'https://github.com/tushar5526/test-project-name.git', 'docker-compose.yml', 'POST')" + ) + assert repr(deployment_config) == expected_repr