diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index b7582de0..8ceb69c6 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -61,12 +61,21 @@ jobs: - name: Install dependencies run: poetry install --no-interaction --no-root - - name: Build binary + - name: Build default binary run: poetry run pyinstaller -F --distpath ./app/bundled_app/linux --specpath ./app/build/linux --workpath ./app/build/linux --paths=./.venv/lib/python3.9/site-packages ./app/pilotcli.py -n ${{ github.sha }} - - name: Rename output file + - name: Rename default output file run: mv "./app/bundled_app/linux/${{ github.sha }}" "./app/bundled_app/linux/pilotcli_linux" + - name: Enable cloud mode + run: touch ./app/ENABLE_CLOUD_MODE + + - name: Build cloud binary + run: poetry run pyinstaller -F --distpath ./app/bundled_app/linux --specpath ./app/build/linux --workpath ./app/build/linux --paths=./.venv/lib/python3.9/site-packages --add-binary=$(pwd)/app/ENABLE_CLOUD_MODE:. ./app/pilotcli.py -n ${{ github.sha }} + + - name: Rename cloud output file + run: mv "./app/bundled_app/linux/${{ github.sha }}" "./app/bundled_app/linux/pilotcli_cloud" + - name: Set version in env run: poetry run echo "TAG_VERSION=`poetry version --short`" >> $GITHUB_ENV @@ -82,7 +91,9 @@ jobs: draft: false prerelease: false target_commitish: ${{ needs.extract-branch-name.outputs.branch }} - files: ./app/bundled_app/linux/pilotcli_linux + files: | + ./app/bundled_app/linux/pilotcli_linux + ./app/bundled_app/linux/pilotcli_cloud push-binary-macos: needs: [ push-binary-linux ] diff --git a/app/configs/user_config.py b/app/configs/user_config.py index 3b80384f..7deb3672 100644 --- a/app/configs/user_config.py +++ b/app/configs/user_config.py @@ -5,6 +5,7 @@ import configparser import os import stat +import sys import time from pathlib import Path from typing import Iterable @@ -26,11 +27,27 @@ class UserConfig(metaclass=Singleton): This user config is global. """ - def __init__(self, config_path: Union[str, Path, None] = None, config_filename: Union[str, None] = None) -> None: + def __init__( + self, + config_path: Union[str, Path, None] = None, + config_filename: Union[str, None] = None, + is_cloud_mode: Union[bool, None] = None, + ) -> None: + """When `is_cloud_mode` is enabled, it omits the checks for file or folder ownership and correct access mode for + the user. + + This adjustment is made to prevent complications with mounted NFS volumes where all files have root ownership. + """ + if config_path is None: config_path = ConfigClass.config_path if config_filename is None: config_filename = ConfigClass.config_file + if is_cloud_mode is None: + # Check when code is bundled using pyinstaller + # https://pyinstaller.org/en/stable/runtime-information.html#run-time-information + is_bundled = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') + is_cloud_mode = is_bundled and (Path(sys._MEIPASS) / 'ENABLE_CLOUD_MODE').is_file() config_path = Path(config_path) if not config_path.exists(): @@ -39,7 +56,7 @@ def __init__(self, config_path: Union[str, Path, None] = None, config_filename: current_user_id = os.geteuid() error = self._check_user_permissions(config_path, current_user_id, (0o0500, 0o0700)) - if error: + if error and not is_cloud_mode: SrvErrorHandler.customized_handle(ECustomizedError.CONFIG_INVALID_PERMISSIONS, True, error) return @@ -48,10 +65,11 @@ def __init__(self, config_path: Union[str, Path, None] = None, config_filename: config_file.touch(mode=0o0600, exist_ok=False) error = self._check_user_permissions(config_file, current_user_id, (0o0400, 0o0600)) - if error: + if error and not is_cloud_mode: SrvErrorHandler.customized_handle(ECustomizedError.CONFIG_INVALID_PERMISSIONS, True, error) return + self.is_cloud_mode = is_cloud_mode self.config_file = config_file self.config = configparser.ConfigParser() self.config.read(self.config_file) diff --git a/tests/app/configs/test_user_config.py b/tests/app/configs/test_user_config.py index ed8c3aeb..71b73970 100644 --- a/tests/app/configs/test_user_config.py +++ b/tests/app/configs/test_user_config.py @@ -4,6 +4,7 @@ import os import stat +import sys import pytest @@ -73,3 +74,42 @@ def test__init__exits_with_error_when_config_file_does_not_have_expected_access_ ) error_log.assert_called_with(expected_message) + + def test__init__does_not_exit_with_error_when_config_folder_has_invalid_access_mode_and_is_cloud_mode_set_to_true( + self, tmp_path, fake + ): + config_folder = tmp_path / fake.pystr() + config_folder.mkdir(mode=0o0755) + + UserConfig(config_folder, is_cloud_mode=True) + + def test__init__does_not_exit_with_error_when_config_file_has_invalid_access_mode_and_is_cloud_mode_set_to_true( + self, tmp_path, fake + ): + config_folder = tmp_path / fake.pystr() + file_name = fake.pystr() + config_file = config_folder / file_name + config_folder.mkdir(mode=0o0700) + config_file.touch(mode=0o0644) + + UserConfig(config_folder, file_name, is_cloud_mode=True) + + def test__init__sets_is_cloud_mode_to_false_by_default(self, tmp_path, fake): + config_folder = tmp_path / fake.pystr() + + user_config = UserConfig(config_folder) + + assert user_config.is_cloud_mode is False + + def test__init__sets_is_cloud_mode_to_true_when_pyinstaller_bundle_params_are_set_and_cloud_mode_file_is_present( + self, tmp_path, monkeypatch + ): + monkeypatch.setattr(sys, 'frozen', True, raising=False) + monkeypatch.setattr(sys, '_MEIPASS', str(tmp_path), raising=False) + + cloud_mode_file = tmp_path / 'ENABLE_CLOUD_MODE' + cloud_mode_file.touch() + + user_config = UserConfig() + + assert user_config.is_cloud_mode is True