diff --git a/.github/workflows/ci-rust.yaml b/.github/workflows/ci.yaml similarity index 70% rename from .github/workflows/ci-rust.yaml rename to .github/workflows/ci.yaml index 2d9ab9f..904532b 100644 --- a/.github/workflows/ci-rust.yaml +++ b/.github/workflows/ci.yaml @@ -1,10 +1,6 @@ name: Rust CI -on: - push: - paths: - - "rozy/**" - - ".github/workflows/ci-rust.yaml" +on: push jobs: build: @@ -26,10 +22,10 @@ jobs: toolchain: stable target: ${{ matrix.target }} - name: Format - run: cargo fmt --manifest-path rozy/Cargo.toml + run: cargo fmt - name: Clippy - run: cargo clippy --manifest-path rozy/Cargo.toml + run: cargo clippy - if: ${{ contains(matrix.target, '-musl') }} run: sudo apt-get install musl-tools - name: Test - run: cargo test --target=${{ matrix.target }} --manifest-path rozy/Cargo.toml + run: cargo test --target=${{ matrix.target }} diff --git a/.github/workflows/release-rust.yaml b/.github/workflows/release.yaml similarity index 97% rename from .github/workflows/release-rust.yaml rename to .github/workflows/release.yaml index b33288b..d6b26ac 100644 --- a/.github/workflows/release-rust.yaml +++ b/.github/workflows/release.yaml @@ -30,7 +30,7 @@ jobs: - if: ${{ contains(matrix.target, '-musl') }} run: sudo apt-get install musl-tools - name: Build - run: cargo build --release --verbose --target=${{ matrix.target }} --manifest-path rozy/Cargo.toml + run: cargo build --release --verbose --target=${{ matrix.target }} - name: Ensure version is correct run: rozy/target/${{ matrix.target }}/release/ozy --version | grep "$(echo $GITHUB_REF_NAME | cut -c2-)" - name: Rename diff --git a/.gitignore b/.gitignore index 05d3bb2..613ddba 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,7 @@ dmypy.json # VSCode .vscode/ + +# ust +/target +/test_resource/integration/.* diff --git a/rozy/.idea/.gitignore b/.idea/.gitignore similarity index 100% rename from rozy/.idea/.gitignore rename to .idea/.gitignore diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 49f7b87..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml deleted file mode 100644 index dc8a6e9..0000000 --- a/.idea/dbnavigator.xml +++ /dev/null @@ -1,454 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 0000000..68ba0ac --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 912e521..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index c6d6b57..94eab37 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/.idea/ozy.iml b/.idea/ozy.iml index 2c80e12..c254557 100644 --- a/.idea/ozy.iml +++ b/.idea/ozy.iml @@ -1,8 +1,9 @@ - + - + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/rozy/Cargo.lock b/Cargo.lock similarity index 100% rename from rozy/Cargo.lock rename to Cargo.lock diff --git a/rozy/Cargo.toml b/Cargo.toml similarity index 100% rename from rozy/Cargo.toml rename to Cargo.toml diff --git a/README.md b/README.md index 7d15589..cc82100 100644 --- a/README.md +++ b/README.md @@ -129,12 +129,20 @@ This will remove `~/.cache/ozy`! TODO +## Developing locally + +`ozy` is written in Rust. A quick start: +1. Install [`rustup`](https://rustup.rs/) +2. Run `install.sh` +3. Prepend `$HOME/.ozy/bin` to your path to have access to managed apps + +Performance is really only a consideration on the common case of running a ozy-managed binary. This is why we prefer not to introduce asynchrony or even template caching in the `install-all` path. + ## Making a release To make a release of ozy: -* Update the `__version__` in `ozy/__init__.py` -* Ensure the `RELEASE_NOTES.md` are updated +* Ensure the `RELEASE_NOTES.md` are updated (not required) * Push the changes to GitHub * Create a tag of the form `v1.2.3` and push it to GitHub * GH actions will make the binaries automatically diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b45890d..1ef4233 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,71 +1,13 @@ # ozy release notes -## 0.0.14 -* Add support for ozy self-update +All changes are in the [Releases](https://github.com/aquanauts/ozy/releases). -## 0.0.13 -* changed CondaInstaller to work with micromamba -* added empty `__init__.py` files in test directory tree to appease vscode +### [0.1.11](https://github.com/aquanauts/ozy/releases/tag/v0.1.11) +* Use `is_file` to avoid "is a directory error" (fixing #66) +* Support symbolic link installations. -## 0.0.12 -* Create a lockfile per installation to allow more concurrency -* Fix concurrent conda installations -* Add list command +### [0.1.10](https://github.com/aquanauts/ozy/releases/tag/v0.1.10) +* Support for passing environment variables -## 0.0.11 -* Fix race condition related to concurrent installations. - -## 0.0.10 -* Fix for detecting `ozy`. Basically, 0.0.6-0.0.9 were broken once packaged. - -## 0.0.9 -* Build linux on Ubuntu 18.04 so we can run the release binaries on Ubuntu 18.04 - -## 0.0.8 -* Add support for `pyinstaller`-compressed `conda` binaries. Set `pyinstaller: True` to have `ozy` install the conda binary in a temporary place, then squash it with pyinstaller, and use the squashed version instead. - -## 0.0.7 -* Add support for `zip` file releases - -## 0.0.6 -* Adds support for post installation commands -* New command-line "install" to force install a subset of apps -* Support for OSX - -## 0.0.5 -* Add support for `pip` installers (via `conda`). - -## 0.0.4 -* Fix logging crash on `ozy install-all` - -## 0.0.3 -* Fix for `ozy` running python programs: `PYTHONPATH` and `LD_LIBRARY_PATH` were left hijacked by - `ozy`'s package system (pyinstaller). - -## 0.0.2 -* Bug fix in makefile-config - - -## 0.0.1-pre (First test release) -* Support makefile-config - -## 0.0.0 (Work in progress) -* Init, info, update implemented -* Apps! Support for `nomad`, `terraform`, and `vault` (general support for any Hashicorp thing) -* Working [README.md](README.md) -* One-line installs a la lake-client -* Supports installing `conda` packages - - ---- - -TODO -* Check the shas and the signed security files -* Support `rm` -* Support `clean` -* Open source! -* "flock" for multiple ozy invocations at once? - -More Apps! -* `docker` -* `sshfs` +### Older releases +See the Releases page. diff --git a/bin/ozy b/bin/ozy deleted file mode 100755 index 6b4bf2f..0000000 --- a/bin/ozy +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python - -import os -import sys - -if getattr(sys, 'frozen', False): - # Running under pyinstaller - from ozy.__main__ import app_main - - # https://pyinstaller.readthedocs.io/en/stable/runtime-information.html#using-sys-executable-and-sys-argv-0 - # pyinstaller may override LD_LIBRARY_PATH, but if it does so, it saves it in LD_LIBRARY_PATH_ORIG - app_main(sys.executable, sys.argv[0], sys.argv[1:], True) -else: - # Running in dev mode - OZY_DIR = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')) - - OZY_PYTHON = os.path.join(OZY_DIR, '.venv', 'bin', 'python3') - environment = os.environ.copy() - # To ensure we work like pyinstaller, we preserve the "original" LD_LIBRARY_PATH here even though we don't modify - # it. We _do_ modify PYTHONPATH so we save that here. - for possibly_overridden in ('PYTHONPATH', 'LD_LIBRARY_PATH'): - if possibly_overridden in environment: - environment[possibly_overridden + '_ORIG'] = environment[possibly_overridden] - environment['PYTHONPATH'] = OZY_DIR - # We pass argv[0] again to the python executable: if we don't use OZY_PYTHON as argv[0], virtualenv flips out. - # As the ozy script needs to know the name it was invoked as, we pass all of argv again. - # i.e. if invoked as "nomad --version" we will "path/to/python path/to/ozy/__main__.py nomad --version - os.execve(OZY_PYTHON, [OZY_PYTHON, os.path.join(OZY_DIR, 'ozy', '__main__.py')] + sys.argv, environment) diff --git a/rozy/install.sh b/install.sh similarity index 100% rename from rozy/install.sh rename to install.sh diff --git a/ozy/__init__.py b/ozy/__init__.py deleted file mode 100644 index e9ccf40..0000000 --- a/ozy/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -__version__ = '0.0.14' -import logging - -_LOGGER = logging.getLogger(__name__) - - -class OzyError(Exception): - pass diff --git a/ozy/__main__.py b/ozy/__main__.py deleted file mode 100644 index 9f7eb9d..0000000 --- a/ozy/__main__.py +++ /dev/null @@ -1,323 +0,0 @@ -import logging -import os -from pathlib import Path -import shutil -import sys - -from packaging import version - -import click -import coloredlogs - -from ozy import OzyError, __version__ -from ozy.app import App, find_app -from ozy.config import load_ozy_user_conf, save_ozy_user_conf, parse_ozy_conf, load_config, safe_expand -from ozy.files import ensure_ozy_dirs, get_ozy_bin_dir, softlink, get_ozy_dir -from ozy.utils import download_to, restore_overridden_env_vars - -_LOGGER = logging.getLogger(__name__) - -PATH_TO_ME = None # TODO find a better way -IS_SINGLE_FILE = False # TODO find a better way - - -def _print_version(ctx, param, value): - if not value or ctx.resilient_parsing: - return - click.echo(f'ozy v{__version__}') - ctx.exit() - - -@click.group() -@click.option("--debug/--no-debug", default=False) -@click.option('--version', is_flag=True, callback=_print_version, expose_value=False, is_eager=True) -def main(debug): - # TODO detect if redirected and don't do this, etc - coloredlogs.install( - fmt='%(message)s', - level=debug and 'DEBUG' or 'INFO') - - -@main.command() -@click.argument("url", metavar="URL", type=str) -def init(url): - """Initialise and install ozy, with configuration from the given URL.""" - ensure_ozy_dirs() - user_conf = load_ozy_user_conf() - # TODO: make sure it isn't there already? upgrade/update instead? - ozy_conf_filename = f"{get_ozy_dir()}/ozy.yaml" - download_to(ozy_conf_filename, url) - ozy_bin_dir = get_ozy_bin_dir() - root_conf = parse_ozy_conf(ozy_conf_filename) ## TODO think how this interacts with local config files - - symlink_binaries(ozy_bin_dir, root_conf) - user_conf['url'] = url - save_ozy_user_conf(user_conf) - - if not check_path(ozy_bin_dir): - _LOGGER.info("ozy is installed, but needs a little more setup work:") - show_path_warning(ozy_bin_dir) - else: - _LOGGER.info("ozy is installed and is ready to run") - - -def symlink_binaries(ozy_bin_dir, config): - global PATH_TO_ME, IS_SINGLE_FILE - if IS_SINGLE_FILE: - dest_filename = os.path.join(ozy_bin_dir, 'ozy') - if os.path.exists(dest_filename) and os.path.samefile(PATH_TO_ME, dest_filename): - _LOGGER.debug("Not copying anything as we're already in the right place") - else: - _LOGGER.debug("Copying single-file ozy distribution") - if os.path.exists(dest_filename): - os.unlink(dest_filename) - shutil.copyfile(PATH_TO_ME, dest_filename) - shutil.copymode(PATH_TO_ME, dest_filename) - else: - _LOGGER.debug("Symlinking dev ozy") - softlink(from_command=PATH_TO_ME, to_command='ozy', ozy_bin_dir=ozy_bin_dir) - for app in config['apps']: - if not softlink(from_command='ozy', to_command=app, ozy_bin_dir=ozy_bin_dir): - _LOGGER.info("Supporting app '%s'", app) - - -def show_path_warning(ozy_bin_dir): - _LOGGER.warning("-" * 80) - _LOGGER.warning("Please ensure '%s' is on your path", ozy_bin_dir) - _LOGGER.info("bash shell users:") - _LOGGER.info(" bash$ echo -e '# ozy support\\nexport PATH=%s:$PATH' >> ~/.bashrc", ozy_bin_dir) - _LOGGER.info(" then restart your shell sessions") - _LOGGER.info("zsh shell users:") - _LOGGER.info(f" zsh$ # path+=({ozy_bin_dir})\\nexport PATH") - _LOGGER.info("fish shell users: ") - _LOGGER.info(" fish$ set --universal fish_user_paths %s $fish_user_paths", ozy_bin_dir) - _LOGGER.warning("-" * 80) - - -def check_path(ozy_bin_dir): - real_paths = set(os.path.realpath(path) for path in os.getenv("PATH").split(":")) - if os.path.realpath(ozy_bin_dir) in real_paths: - return True - return False - - -@main.command() -@click.option("--url", metavar="URL", type=str, help="configuration URL (default will use previously set)") -@click.option("--dry-run/--no-dry-run", help="make no changes, just show what would happen", default=False) -def update(dry_run, url): - """Update base configuration from the remote URL.""" - user_conf = load_ozy_user_conf() - if not url: - if 'url' not in user_conf: - raise OzyError('Missing url in configuration') - url = user_conf['url'] - ozy_conf_filename = f"{get_ozy_dir()}/ozy.yaml" - tmp_filename = ozy_conf_filename + ".tmp" - download_to(tmp_filename, url) - new_conf_root = parse_ozy_conf(tmp_filename) - old_conf_root = parse_ozy_conf(ozy_conf_filename) - - new_ozy_version: str = new_conf_root['ozy_version'] - if version.parse(__version__) < version.parse(new_ozy_version): - _LOGGER.info('Ozy update to %s is mandated by your team config', new_ozy_version) - download_url = safe_expand(dict(version=new_ozy_version), new_conf_root['ozy_download']) - _LOGGER.info('Downloading from %s', download_url) - download_path = Path(get_ozy_dir()) / 'bin' / 'ozy.tmp' - download_path.unlink(missing_ok=True) - download_to(str(download_path), download_url) - download_path.chmod(0o755) - new_ozy_exe = str(download_path) - environment = restore_overridden_env_vars(os.environ) - os.execve(new_ozy_exe, [new_ozy_exe, 'update'], environment) - - changed = False - for app, new_conf in new_conf_root['apps'].items(): - old_conf = old_conf_root['apps'].get(app, None) - if not old_conf: - _LOGGER.info('%s new app %s (%s)', "Would install" if dry_run else "Installing", app, new_conf['version']) - changed = True - elif old_conf['version'] != new_conf['version']: - _LOGGER.info('%s %s from %s to %s', "Would upgrade" if dry_run else "Upgrading", app, old_conf['version'], - new_conf['version']) - changed = True - - if not dry_run: - ozy_bin_dir = get_ozy_bin_dir() - user_conf['url'] = url - save_ozy_user_conf(user_conf) - os.rename(tmp_filename, ozy_conf_filename) - symlink_binaries(ozy_bin_dir, new_conf_root) - if not changed: - _LOGGER.info("No changes made") - else: - if changed: - _LOGGER.info("Dry run only - no changes made") - else: - _LOGGER.info("Dry run only - no changes would be made, even without --dry-run") - os.unlink(tmp_filename) - - -@main.command() -def info(): - """Print information about the installation and configuration.""" - _LOGGER.info(f"ozy v{__version__}") - ozy_bin_dir = get_ozy_bin_dir() - path_ok = check_path(ozy_bin_dir) - if not path_ok: - show_path_warning(ozy_bin_dir) - user_config = load_ozy_user_conf() - _LOGGER.info("Team URL: %s", user_config.get("url", "(unset)")) - config = load_config() - _LOGGER.info("Team config name: %s", config.get("name", "(unset)")) - for app_name in config['apps']: - app = App(app_name, config) - _LOGGER.info(" %s: %s", app_name, app) - if path_ok: - found_app = shutil.which(app_name) - if not found_app: - _LOGGER.warning(" %s not found on path: perhaps an 'ozy sync' is needed?", app_name) - else: - if os.path.realpath(found_app) != os.path.realpath(os.path.join(ozy_bin_dir, app_name)): - _LOGGER.warning(" %s is not under ozy control! It was found on your PATH earlier than ozy at %s", - app_name, found_app) - - -@main.command(name="list") -def list_cmd(): - """List all known package namess.""" - config = load_config() - for app_name in config['apps']: - click.echo(app_name) - - -@main.command() -def install_all(): - """Ensures all applications are installed at their current prevailing versions.""" - config = load_config() - for app_name in config['apps']: - app = App(app_name, config) - app.ensure_installed() - - -@main.command() -@click.argument("apps", metavar="APP...", nargs=-1, required=True, type=str) -def install(apps): - """Ensures the named applications are installed at their current prevailing versions.""" - config = load_config() - for app_name in apps: - if app_name not in config['apps']: - raise OzyError(f"App '{app_name}' was not found") - app = App(app_name, config) - app.ensure_installed() - - -@main.command() -def sync(): - """ - Synchronise any local changes. - - If you're defining new applications in local user files, you can use this to ensure - the relevant symlinks are created in your ozy bin directory. - """ - symlink_binaries(get_ozy_bin_dir(), load_config()) - - -def _makefile_error(error): - print(f'$(error "{error}")') # todo escape - sys.exit(1) - - -@main.command() -@click.option("--all-apps/--no-all-apps", help="include all APPs", default=False) -@click.argument("makefile_var", metavar="VAR", type=str) -@click.argument("required_apps", metavar="APP", nargs=-1, type=str) -def makefile_config(makefile_var, required_apps, all_apps): - """ - Checks apps, and prints a single-line Makefile variable. - - Use as an argument to $(eval). Errors will are output as $(error) directives - to report in make. - The given variable is defined to be the ozy binary directory, so any app will be - $(VAR)/app_name. If undefined, you know ozy isn't installed. - - Example: - - \b - $ cat Makefile - $(eval $(shell ozy makefile-config OZY_BIN_DIR terraform)) - ifndef OZY_BIN_DIR - $(error please install ozy) - endif - - \b - install: - $(OZY_BIN_DIR)/terraform apply - """ - config = load_config() - ozy_bin_dir = get_ozy_bin_dir() - path_ok = check_path(ozy_bin_dir) - if not path_ok: - _makefile_error("ozy is not on the path") - if all_apps: - required_apps = config['apps'].keys() - if not required_apps: - _makefile_error("no ozy apps found to configure") - for app_name in required_apps: - if app_name not in config['apps']: - _makefile_error(f"Missing ozy app '{app_name}'") - app = App(app_name, config) - found_app = shutil.which(app_name) - app_in_bin = os.path.join(ozy_bin_dir, app_name) - if os.path.realpath(found_app) != os.path.realpath(app_in_bin): - _makefile_error(f"{found_app} found in PATH earlier than ozy: " - f"results could be inconsistent (found at {found_app})") - print(f"{makefile_var}:={ozy_bin_dir}") - - -@main.command() -@click.option("--version", metavar="VERSION", type=str) -@click.argument("app", metavar="APP", type=str) -@click.argument("arguments", metavar="ARG", nargs=-1, type=str) -def run(app, arguments, version): - """Runs the given application.""" - _run(app, arguments, version) - - -def _run(app, arguments, version=None): - tool = find_app(app, version) - if not tool: - raise OzyError(f"Unable to find ozy-controlled app '{app}'") - tool.ensure_installed() - try: - # The child process shouldn't get any of our overridden variables; put the original ones back. - environment = restore_overridden_env_vars(os.environ) - os.execve(tool.executable, [tool.executable] + list(arguments), environment) - except Exception as e: - _LOGGER.error("Unable to execute %s: %s", tool.executable, e) - raise - - -def app_main(path_to_ozy, argv0, arguments, is_single_file): - global PATH_TO_ME, IS_SINGLE_FILE - PATH_TO_ME = os.path.realpath(path_to_ozy) - IS_SINGLE_FILE = is_single_file - - invoked_as = os.path.basename(argv0) - # If we were invoked as "ourself" (or something prefixed with ozy, to cover `ozy-Linux_x86_64` etc) then run as if - # it was 'ozy'. We allow for anything called 'ozy' to be "us", rather than relying on the `argv0` being a symlink or - # similar: under pyinstaller we lose the full path to the original executable, so can't check if it's "really" ozy - # being called directly, or a symlink to ozy. - if invoked_as.startswith('ozy'): - main(prog_name='ozy', args=arguments) - else: - coloredlogs.install(fmt='%(message)s', level='INFO') - _run(invoked_as, arguments) - - -if __name__ == "__main__": - try: - app_main(sys.argv[1], sys.argv[1], sys.argv[2:], False) - except OzyError as ozy_error: - _LOGGER.error(ozy_error) - _LOGGER.debug(ozy_error, exc_info=True) - sys.exit(1) diff --git a/ozy/app.py b/ozy/app.py deleted file mode 100644 index b9c2654..0000000 --- a/ozy/app.py +++ /dev/null @@ -1,121 +0,0 @@ -import fcntl -import logging -import os -import shutil -import shlex -from subprocess import check_call -from typing import Union, List -import uuid - -from ozy import OzyError -from ozy.config import resolve, load_config -from ozy.files import get_ozy_cache_dir -from ozy.installers import SUPPORTED_INSTALLERS - -_LOGGER = logging.getLogger(__name__) - - -def ensure_keys(name, config, *keys): - values = [] - for required_key in keys: - if required_key not in config: - raise OzyError(f"Missing required key '{required_key}' in '{name}'") - values.append(config[required_key]) - return values - - -def _fixup_one_command(command): - if isinstance(command, list): - return command - return shlex.split(command) - - -def fixup_post_install(post_install: Union[list, str]) -> List[List[str]]: - if not post_install: - return [] - if isinstance(post_install, str): - return [_fixup_one_command(post_install)] - post_install = [_fixup_one_command(x) for x in post_install] - return post_install - - -class App: - def __init__(self, name, root_config): - self._name = name - self._root_config = root_config - self._config = resolve(root_config['apps'][name], self._root_config.get('templates', {})) - self._executable_path = self._config.get('executable_path', self.name) - self._relocatable = self._config.get('relocatable', True) - self._post_install = fixup_post_install(self._config.get('post_install', [])) - self._version, install_type = ensure_keys(name, self._config, 'version', 'type') - if install_type not in SUPPORTED_INSTALLERS: - raise OzyError(f"Unsupported installation type '{install_type}'") - self._installer = SUPPORTED_INSTALLERS[install_type](name, self._config) - - def __str__(self): - return f'{self.name} {self._version} ({self._installer})' - - @property - def name(self) -> str: - return self._name - - @property - def config(self) -> dict: - return self._config - - @property - def version(self) -> str: - return self._version - - @property - def install_path(self) -> str: - return os.path.join(get_ozy_cache_dir(), self.name, self.version) - - @property - def executable(self) -> str: - return os.path.join(self.install_path, self._executable_path) - - def is_installed(self) -> bool: - return os.path.isdir(self.install_path) - - def _install(self): - _LOGGER.info("Installing %s %s", self.name, self.version) - uniq_install_dir = f"{self.install_path}.{uuid.uuid4()}" - try: - self._installer.install(uniq_install_dir) - if self._relocatable: - os.rename(uniq_install_dir, self.install_path) - else: - os.symlink(uniq_install_dir, self.install_path) - except Exception: - shutil.rmtree(uniq_install_dir, ignore_errors=True) - raise - - for install_step in self._post_install: - _LOGGER.info("Running post install step '%s'", " ".join(install_step)) - check_call(install_step, cwd=self.install_path) - - def ensure_installed(self): - if self.is_installed(): return - - lockfile_path = f"{self.install_path}.lock" - os.makedirs(os.path.dirname(lockfile_path), exist_ok=True) - with open(lockfile_path, 'w') as lockfile: - try: - fcntl.flock(lockfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) - except BlockingIOError: - _LOGGER.info("Waiting for concurrent install to complete...") - fcntl.flock(lockfile.fileno(), fcntl.LOCK_EX) - if not self.is_installed(): - self._install() - - -def find_app(tool, version=None): - overrides = None - if version: - overrides = dict(apps=dict(tool=dict(version=version))) - config = load_config(overrides) - if tool in config['apps']: - return App(tool, config) - else: - return None diff --git a/ozy/config.py b/ozy/config.py deleted file mode 100644 index ba99ede..0000000 --- a/ozy/config.py +++ /dev/null @@ -1,100 +0,0 @@ -import logging -import os -from collections import ChainMap - -import yaml - -from ozy import OzyError -from ozy.files import walk_up_dirs, get_ozy_dir - -_LOGGER = logging.getLogger(__name__) - - -def load_config(overrides=None, config=None, current_working_directory=None): - if config is None: - config = parse_ozy_conf(f"{get_ozy_dir()}/ozy.yaml") - if current_working_directory is None: - current_working_directory = os.getcwd() - - # Annoyingly can't just use a chainmap here as nested maps don't work the way we'd like - ozy_files = [] - for path in walk_up_dirs(current_working_directory): - conf_file = os.path.join(path, '.ozy.yaml') - if os.path.isfile(conf_file): - ozy_files.append(conf_file) - for path in reversed(ozy_files): - apply_overrides(parse_ozy_conf(path), config) - if overrides: - apply_overrides(overrides, config) - _LOGGER.debug(config) - return config - - -def apply_overrides(source, destination): - for key, value in source.items(): - if isinstance(value, dict): - node = destination.setdefault(key, {}) - apply_overrides(value, node) - else: - destination[key] = value - - return destination - - -def resolve(config, templates): - if 'template' in config: - template_name = config['template'] - if template_name not in templates: - raise OzyError(f"Unable to find template '{template_name}'") - # TODO had these the wrong way round to start with. make a test - config = ChainMap(config, - templates[template_name]) - return {key: safe_expand(config, value) for key, value in config.items()} - - -def safe_expand(format_params, to_expand): - if isinstance(to_expand, list): - return [safe_expand(format_params, x) for x in to_expand] - elif not isinstance(to_expand, str): - return to_expand - - params = get_system_variables() - params.update(format_params) - try: - return to_expand.format(**params) - except KeyError as ke: - raise OzyError(f"Could not find key {ke} in expansion '{to_expand}' with params '{format_params}'") - - -def load_ozy_user_conf(): - user_conf_file = get_user_conf_file() - user_conf = dict() - if os.path.exists(user_conf_file): - user_conf = parse_ozy_conf(user_conf_file) - return user_conf - - -def save_ozy_user_conf(config): - with open(get_user_conf_file(), 'w') as user_conf_file_obj: - yaml.dump(config, user_conf_file_obj) - - -def get_user_conf_file(): - user_conf_file = f"{get_ozy_dir()}/ozy.user.yaml" - return user_conf_file - - -def parse_ozy_conf(ozy_file_name): - _LOGGER.debug("Parsing config %s", ozy_file_name) - with open(ozy_file_name, "r") as ofnh: - yaml_content = yaml.load(ofnh, Loader=yaml.UnsafeLoader) - return yaml_content - - -def get_system_variables(): - uname = os.uname() - return { - 'ozy_os': uname.sysname.lower(), - 'ozy_machine': uname.machine, - 'ozy_arch': 'amd64' if uname.machine == 'x86_64' else uname.machine - } diff --git a/ozy/files.py b/ozy/files.py deleted file mode 100644 index 5dec8ab..0000000 --- a/ozy/files.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging -import os - -from ozy import OzyError - -_LOGGER = logging.getLogger(__name__) - - -def walk_up_dirs(path): - path = os.path.realpath(path) - previous_path = None - while path != previous_path: - yield path - previous_path = path - path = os.path.realpath(os.path.join(path, '..')) - - -def ensure_ozy_dirs(): - [os.makedirs(p, exist_ok=True) for p in (get_ozy_dir(), get_ozy_bin_dir(), get_ozy_cache_dir())] - - -def get_home_dir() -> str: - if 'HOME' in os.environ: - return os.environ['HOME'] - raise OzyError("HOME env variable not found") - - -def get_ozy_dir() -> str: - return f"{get_home_dir()}/.ozy" - - -def get_ozy_cache_dir() -> str: - return os.path.join(os.getenv('XDG_CACHE_HOME', f"{get_home_dir()}/.cache"), 'ozy') - - -def get_ozy_bin_dir() -> str: - return os.path.join(get_ozy_dir(), "bin") - - -def softlink(from_command, to_command, ozy_bin_dir): - # assume this linkage will ONLY be called by ozy - path_to_app = os.path.join(ozy_bin_dir, to_command) - was_there = os.path.exists(path_to_app) - if was_there: - _LOGGER.debug(f"Clobbering symlink path {path_to_app}") - os.unlink(path_to_app) - os.symlink(from_command, path_to_app) - return was_there diff --git a/ozy/installer.py b/ozy/installer.py deleted file mode 100644 index 6ebb0af..0000000 --- a/ozy/installer.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Any - -from ozy import OzyError - - -class Installer: - def __init__(self, name: str, config: dict, *required_keys, **default_keys): - self._name = name - self._config = default_keys.copy() - self._executable_path = config.get('executable_path', name) - - for required_key in required_keys: - if required_key not in config: - raise OzyError(f"Missing required key '{required_key}' in '{name}'") - self._config[required_key] = config[required_key] - for optional_key in default_keys.keys(): - if optional_key in config: - self._config[optional_key] = config[optional_key] - - def config(self, name) -> Any: - return self._config[name] - - def install(self, to_dir: str): - raise RuntimeError("Must be overridden") - -# TODO tests for installers! diff --git a/ozy/installers/__init__.py b/ozy/installers/__init__.py deleted file mode 100644 index df26be3..0000000 --- a/ozy/installers/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from ozy.installers.conda import CondaInstaller -from ozy.installers.file import SingleFileInstaller -from ozy.installers.pip import PipInstaller -from ozy.installers.shell import ShellInstaller -from ozy.installers.tar import TarballInstaller -from ozy.installers.zip import SingleBinaryZipInstaller, ZipInstaller - -SUPPORTED_INSTALLERS = dict( - single_binary_zip=SingleBinaryZipInstaller, - zip=ZipInstaller, - tarball=TarballInstaller, - single_file=SingleFileInstaller, - shell_install=ShellInstaller, - conda=CondaInstaller, - pip=PipInstaller -) diff --git a/ozy/installers/conda.py b/ozy/installers/conda.py deleted file mode 100644 index 4df93f7..0000000 --- a/ozy/installers/conda.py +++ /dev/null @@ -1,53 +0,0 @@ -import logging -import os -from pathlib import Path -from subprocess import check_call -from tempfile import TemporaryDirectory -from typing import List - -from ozy.installer import Installer - -_LOGGER = logging.getLogger(__name__) - - -def do_conda_install(conda_bin, channels, to_dir, to_install): - Path(to_dir).parent.mkdir(parents=True, exist_ok=True) - channels_args = [] - for channel in channels: - channels_args += ['-c', channel] - with TemporaryDirectory(dir=Path(to_dir).parent) as conda_cache_dir: - env = os.environ.copy() - env['CONDA_PKGS_DIRS'] = conda_cache_dir - args = [conda_bin, 'create', '-y'] + channels_args + ['-p', to_dir] + to_install - _LOGGER.debug("Executing %s", " ".join(args)) - check_call(args, env=env) - - -class CondaInstaller(Installer): - def __init__(self, name, config): - super().__init__(name, config, 'package', 'version', channels=[], conda_bin="conda", pyinstaller=False) - - def __str__(self): - return f'conda app installer for {self.config("package")}' - - def _conda_install(self, to_dir: str, to_install: List[str]): - do_conda_install(self.config("conda_bin"), self.config('channels'), to_dir, to_install) - - def install(self, to_dir): - versioned_package = f'{self.config("package")}={self.config("version")}' - if self.config('pyinstaller'): - with TemporaryDirectory(prefix='ozy-conda-installer') as td: - self._conda_install(td, [versioned_package, 'pyinstaller']) - pyinst = Path(td) / 'bin' / 'pyinstaller' - dest = (Path(to_dir) / self._executable_path).parent - args = [ - str(pyinst), - '--onefile', - '--name', self._name, - '--distpath', str(dest), - str(Path(td) / self._executable_path) - ] - _LOGGER.debug("Executing %s", " ".join(args)) - check_call(args) - else: - self._conda_install(to_dir, [versioned_package]) diff --git a/ozy/installers/file.py b/ozy/installers/file.py deleted file mode 100644 index 0fd5b83..0000000 --- a/ozy/installers/file.py +++ /dev/null @@ -1,20 +0,0 @@ -import os - -from ozy.installer import Installer -from ozy.utils import download_to_file_obj - - -class SingleFileInstaller(Installer): - def __init__(self, name, config): - super().__init__(name, config, 'url', app_name=name) - - def __str__(self): - return f'file installer from {self.config("url")}' - - def install(self, to_dir): - os.makedirs(to_dir) - url = self.config('url') - app_path = os.path.join(to_dir, self.config('app_name')) - with open(app_path, 'wb') as output_file: - download_to_file_obj(output_file, url) - os.chmod(app_path, 0o774) diff --git a/ozy/installers/pip.py b/ozy/installers/pip.py deleted file mode 100644 index 07bebcc..0000000 --- a/ozy/installers/pip.py +++ /dev/null @@ -1,24 +0,0 @@ -import logging -import os -from subprocess import check_call - -from ozy.installer import Installer -from ozy.installers.conda import do_conda_install - -_LOGGER = logging.getLogger(__name__) - - -class PipInstaller(Installer): - def __init__(self, name, config): - super().__init__(name, config, 'package', 'version', channels=[]) - - def __str__(self): - return f'pip app installer for {self.config("package")}' - - def install(self, to_dir): - # Create a conda environment with pip installed (which brings in python et al) - do_conda_install('conda', self.config('channels'), to_dir, ['pip']) - # Use that environment to pip install the user's package - args = [os.path.join(to_dir, 'bin', 'pip'), 'install', f'{self.config("package")}=={self.config("version")}'] - _LOGGER.debug("Executing %s", " ".join(args)) - check_call(args) diff --git a/ozy/installers/shell.py b/ozy/installers/shell.py deleted file mode 100644 index 778effb..0000000 --- a/ozy/installers/shell.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -from subprocess import check_call -from tempfile import NamedTemporaryFile - -from ozy.installer import Installer -from ozy.utils import download_to_file_obj - - -class ShellInstaller(Installer): - def __init__(self, name, config): - super().__init__(name, config, 'url', 'shell_args', extra_path_during_install='') - - def __str__(self): - return f'shell file installer from {self.config("url")}' - - def install(self, to_dir): - os.makedirs(to_dir) - url = self.config('url') - with NamedTemporaryFile(delete=False) as temp_file: - download_to_file_obj(temp_file, url) - temp_file.close() - env = os.environ.copy() - extra_path_during_install = self.config('extra_path_during_install') - if extra_path_during_install: - extra_path_during_install = os.path.join(to_dir, extra_path_during_install) - env['PATH'] = f"{extra_path_during_install}:{env['PATH']}" - env['INSTALL_DIR'] = to_dir - check_call(["/bin/bash", temp_file.name] + self.config("shell_args"), env=env) - os.unlink(temp_file.name) diff --git a/ozy/installers/tar.py b/ozy/installers/tar.py deleted file mode 100644 index f0c10ad..0000000 --- a/ozy/installers/tar.py +++ /dev/null @@ -1,23 +0,0 @@ -import os -import tarfile -from tempfile import NamedTemporaryFile - -from ozy.installer import Installer -from ozy.utils import download_to_file_obj - - -class TarballInstaller(Installer): - def __init__(self, name, config): - super().__init__(name, config, 'url') - - def __str__(self): - return f'tar installer from {self.config("url")}' - - def install(self, to_dir): - os.makedirs(to_dir) - url = self.config('url') - with NamedTemporaryFile() as temp_file: - download_to_file_obj(temp_file, url) - temp_file.flush() - tf = tarfile.open(temp_file.name, 'r') - tf.extractall(to_dir) diff --git a/ozy/installers/zip.py b/ozy/installers/zip.py deleted file mode 100644 index 6bb6dca..0000000 --- a/ozy/installers/zip.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -from tempfile import NamedTemporaryFile -from zipfile import ZipFile - -from ozy import OzyError -# TODO support sha256, sha256_signature and sha256_gpg_key -from ozy.installer import Installer -from ozy.utils import download_to_file_obj - - -class SingleBinaryZipInstaller(Installer): - def __init__(self, name, config): - super().__init__(name, config, 'url', app_name=name) - - def __str__(self): - return f'single_binary_zip installer from {self.config("url")}' - - def install(self, to_dir): - app_name = self.config('app_name') - url = self.config('url') - os.makedirs(to_dir) - app_path = os.path.join(to_dir, app_name) - with NamedTemporaryFile() as temp_file: - download_to_file_obj(temp_file, url) - temp_file.flush() - zf = ZipFile(temp_file.name) - contents = zf.namelist() - if len(contents) != 1: - raise OzyError(f"More than one file in the zipfile at {url}! ({contents})") - with open(app_path, 'wb') as out_file: - with zf.open(contents[0]) as in_file: - out_file.write(in_file.read()) - os.chmod(app_path, 0o774) - - -class ZipInstaller(Installer): - def __init__(self, name, config): - super().__init__(name, config, 'url') - - def __str__(self): - return f'zip installer from {self.config("url")}' - - def install(self, to_dir): - os.makedirs(to_dir) - url = self.config('url') - with NamedTemporaryFile() as temp_file: - download_to_file_obj(temp_file, url) - temp_file.flush() - zf = ZipFile(temp_file.name) - zf.extractall(to_dir) - - os.chmod(os.path.join(to_dir, self._executable_path), 0o755) diff --git a/ozy/utils.py b/ozy/utils.py deleted file mode 100644 index b19d82b..0000000 --- a/ozy/utils.py +++ /dev/null @@ -1,46 +0,0 @@ -import logging -import os -from typing import BinaryIO - -import requests -from tqdm import tqdm - -from ozy import OzyError - -_LOGGER = logging.getLogger(__name__) -_DOWNLOAD_CHUNK_SIZE = 128 * 1024 - - -def download_to_file_obj(dest_file_obj: BinaryIO, url: str): - response = requests.get(url, stream=True) - if not response.ok: - raise OzyError(f"Unable to fetch url '{url}' - {response}") - total_size = int(response.headers.get('content-length', 0)) - with tqdm(total=total_size, unit='iB', unit_scale=True) as t: - for data in response.iter_content(_DOWNLOAD_CHUNK_SIZE): - t.update(len(data)) - dest_file_obj.write(data) - - -def download_to(dest_file_name: str, url: str): - _LOGGER.debug("Downloading %s to %s", url, dest_file_name) - dest_file_temp = dest_file_name + ".tmp" - try: - with open(dest_file_temp, 'wb') as dest_file_obj: - download_to_file_obj(dest_file_obj, url) - os.rename(dest_file_temp, dest_file_name) - except Exception: - os.unlink(dest_file_temp) - raise - - -def restore_overridden_env_vars(environment): - environment = environment.copy() - for possibly_overridden in ['PYTHONPATH', 'LD_LIBRARY_PATH']: - orig = possibly_overridden + '_ORIG' - if orig in environment: - environment[possibly_overridden] = environment[orig] - del environment[orig] - elif possibly_overridden in environment: - del environment[possibly_overridden] - return environment diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 853bd36..0000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -pytest -pytest_watch - -click -coloredlogs -packaging -pyinstaller -pyyaml -requests -tqdm diff --git a/rozy/.gitignore b/rozy/.gitignore deleted file mode 100644 index 54f9ea1..0000000 --- a/rozy/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -/test_resource/integration/.* diff --git a/rozy/.idea/modules.xml b/rozy/.idea/modules.xml deleted file mode 100644 index 4e398b4..0000000 --- a/rozy/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/rozy/.idea/rozy.iml b/rozy/.idea/rozy.iml deleted file mode 100644 index c254557..0000000 --- a/rozy/.idea/rozy.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/rozy/.idea/vcs.xml b/rozy/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/rozy/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/rozy/README.md b/rozy/README.md deleted file mode 100644 index 19e4d67..0000000 --- a/rozy/README.md +++ /dev/null @@ -1,17 +0,0 @@ -This is a Rust implementation of Ozy. - -## Instructions -1. Install [`rustup`](https://rustup.rs/) -2. Run `install.sh` -4. Prepend `$HOME/.ozy/bin` to your path to have access to managed apps - -Performance is really only a consideration on the common case of running a ozy-managed binary. This is why we prefer not to introduce asynchrony or even template caching in the `install-all` path. - -## Currently implemented commands -* `ozy clean` deletes managed directories -* `ozy init` sets up app symlinks from a base config from a provided URL -* `ozy update` accepts an optional URL, but will use the init-configured one without one -* `ozy install [app name]` installs a single app -* `ozy install-all` installs them all -* `ozy run [app name]` will run that app. Typically, you'll just run `[app name]` in the shell directly -* `ozy makefile-config` output `Makefile` compatible configuration for a list of apps diff --git a/rozy/src/app.rs b/src/app.rs similarity index 100% rename from rozy/src/app.rs rename to src/app.rs diff --git a/rozy/src/config.rs b/src/config.rs similarity index 100% rename from rozy/src/config.rs rename to src/config.rs diff --git a/rozy/src/files.rs b/src/files.rs similarity index 100% rename from rozy/src/files.rs rename to src/files.rs diff --git a/rozy/src/installers/conda.rs b/src/installers/conda.rs similarity index 100% rename from rozy/src/installers/conda.rs rename to src/installers/conda.rs diff --git a/rozy/src/installers/file.rs b/src/installers/file.rs similarity index 100% rename from rozy/src/installers/file.rs rename to src/installers/file.rs diff --git a/rozy/src/installers/installer.rs b/src/installers/installer.rs similarity index 100% rename from rozy/src/installers/installer.rs rename to src/installers/installer.rs diff --git a/rozy/src/installers/mod.rs b/src/installers/mod.rs similarity index 100% rename from rozy/src/installers/mod.rs rename to src/installers/mod.rs diff --git a/rozy/src/installers/pip.rs b/src/installers/pip.rs similarity index 100% rename from rozy/src/installers/pip.rs rename to src/installers/pip.rs diff --git a/rozy/src/installers/shell.rs b/src/installers/shell.rs similarity index 100% rename from rozy/src/installers/shell.rs rename to src/installers/shell.rs diff --git a/rozy/src/installers/single_binary_zip.rs b/src/installers/single_binary_zip.rs similarity index 100% rename from rozy/src/installers/single_binary_zip.rs rename to src/installers/single_binary_zip.rs diff --git a/rozy/src/installers/symlink.rs b/src/installers/symlink.rs similarity index 100% rename from rozy/src/installers/symlink.rs rename to src/installers/symlink.rs diff --git a/rozy/src/installers/tarball.rs b/src/installers/tarball.rs similarity index 100% rename from rozy/src/installers/tarball.rs rename to src/installers/tarball.rs diff --git a/rozy/src/installers/zip.rs b/src/installers/zip.rs similarity index 100% rename from rozy/src/installers/zip.rs rename to src/installers/zip.rs diff --git a/rozy/src/main.rs b/src/main.rs similarity index 100% rename from rozy/src/main.rs rename to src/main.rs diff --git a/rozy/src/utils.rs b/src/utils.rs similarity index 100% rename from rozy/src/utils.rs rename to src/utils.rs diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/smoke/__init__.py b/test/smoke/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/smoke/test_target_config.py b/test/smoke/test_target_config.py deleted file mode 100644 index 77a3184..0000000 --- a/test/smoke/test_target_config.py +++ /dev/null @@ -1,28 +0,0 @@ -import requests - - -def is_valid_int(s): - try: - int(s) - return True - except ValueError: - return False - - -def test_can_validate_a_hashicorp_installer(): - config = { - "version-string": "0.9.4", - "download-url": "https://releases.hashicorp.com/nomad/0.9.4/nomad_0.9.4_linux_amd64.zip", - "download-sha256sum-url": "https://releases.hashicorp.com/nomad/0.9.4/nomad_0.9.4_SHA256SUMS" - } - - response = requests.get(config['download-url'], stream=True) - - assert response.status_code == 200 - - headers = response.headers - content_length = headers['Content-length'] - assert is_valid_int(content_length) - content_length = int(content_length) - assert content_length > 0 - print(content_length) diff --git a/test/unit/__init__.py b/test/unit/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/unit/installers/__init__.py b/test/unit/installers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/unit/installers/test_conda.py b/test/unit/installers/test_conda.py deleted file mode 100644 index e43bfaa..0000000 --- a/test/unit/installers/test_conda.py +++ /dev/null @@ -1,34 +0,0 @@ -import os - -from tempfile import TemporaryDirectory -from unittest import mock - -from ozy.installers import CondaInstaller - - -@mock.patch('ozy.installers.conda.check_call') -def test_should_install_with_regular_installer(mock_check_call): - with TemporaryDirectory() as root: - installer = CondaInstaller('test', dict(package='package', version='1.0.0', channels=['chan1', 'chan2'])) - installer.install(root + '/some/directory') - assert os.path.isdir(root + '/some') - mock_check_call.assert_called_with([ - 'conda', 'create', '-y', - '-c', 'chan1', - '-c', 'chan2', - '-p', root + '/some/directory', - 'package=1.0.0' - ], env=mock.ANY) - -@mock.patch('ozy.installers.conda.do_conda_install') -@mock.patch('ozy.installers.conda.check_call') -def test_should_install_with_pyinstaller_squish(mock_check_call, mock_do_conda_install): - installer = CondaInstaller('test', - dict(package='package', version='1.0.0', pyinstaller=True, channels=['chan1', 'chan2'])) - installer.install('/some/directory') - mock_do_conda_install.assert_called_with('conda', ['chan1', 'chan2'], mock.ANY, ['package=1.0.0', 'pyinstaller']) - mock_check_call.assert_called_with([ - mock.ANY, # the unknown temporary file reference to pyinstaller - '--onefile', '--name', 'test', '--distpath', '/some/directory', - mock.ANY # the unknown temporary file reference to the test installation - ]) diff --git a/test/unit/installers/test_pip.py b/test/unit/installers/test_pip.py deleted file mode 100644 index 03e1558..0000000 --- a/test/unit/installers/test_pip.py +++ /dev/null @@ -1,30 +0,0 @@ -from unittest import mock - -import pytest - -from ozy import OzyError -from ozy.installers.pip import PipInstaller - - -def test_raises_on_missing_keys(): - with pytest.raises(OzyError): - PipInstaller('test', dict()) - with pytest.raises(OzyError): - PipInstaller('test', dict(package='p')) - with pytest.raises(OzyError): - PipInstaller('test', dict(version='123')) - - -def test_constructs_ok_with_correct_config(): - PipInstaller('test', dict(package='p', version='123')) - - -@mock.patch('ozy.installers.pip.do_conda_install') -@mock.patch('ozy.installers.pip.check_call') -def test_installs(mock_check_call, mock_do_conda_install): - installer = PipInstaller('test', dict(package='package', version='1.0.0')) - installer.install('/some/directory') - mock_do_conda_install.assert_called_with('conda', [], '/some/directory', ['pip']) - mock_check_call.assert_has_calls([ - mock.call(['/some/directory/bin/pip', 'install', 'package==1.0.0']) - ]) diff --git a/test/unit/test_app.py b/test/unit/test_app.py deleted file mode 100644 index e53ac9f..0000000 --- a/test/unit/test_app.py +++ /dev/null @@ -1,47 +0,0 @@ -import pytest - -from ozy import OzyError -from ozy.app import App, ensure_keys, fixup_post_install -from ozy.config import parse_ozy_conf, resolve - - -def test_app(): - sample_config_file = "conf/sample-team-conf.yaml" - ozy_conf = parse_ozy_conf(sample_config_file) - app = App("nomad", ozy_conf) - assert app.name == "nomad" - assert app.config['template'] == 'hashicorp' - assert app.config['url'] - assert app.config['type'] == 'single_binary_zip' - assert app.config['sha256_gpg_key'] == ozy_conf['templates']['hashicorp']['sha256_gpg_key'] - assert app.config['sha256'] - assert app.config['sha256_signature'] - - -def test_unsupported_installer(): - sample_config_file = "conf/sample-team-conf.yaml" - ozy_conf = parse_ozy_conf(sample_config_file) - ozy_conf['templates']['hashicorp']['type'] = 'triple_binary_star' # not a supported installer - with pytest.raises(OzyError): - App("nomad", ozy_conf) - - -def test_ensure_keys(): - sample_config_file = "conf/sample-team-conf.yaml" - ozy_conf = parse_ozy_conf(sample_config_file) - - tmp_ozy_conf = ozy_conf.copy() - del tmp_ozy_conf['templates']['hashicorp']['type'] - config = resolve(ozy_conf['apps']['nomad'], ozy_conf['templates']) - assert config - with pytest.raises(OzyError): - ensure_keys('nomad', config, 'type') - - -def test_fixup_post_install(): - assert fixup_post_install("") == [] - assert fixup_post_install("this is a test") == [["this", "is", "a", "test"]] - assert fixup_post_install(["this is a test"]) == [["this", "is", "a", "test"]] - assert fixup_post_install(["this is a test", "two"]) == [["this", "is", "a", "test"], ["two"]] - assert fixup_post_install([["one", "two"], ["three", "four"]]) == [["one", "two"], ["three", "four"]] - assert fixup_post_install("this is 'a test'") == [["this", "is", "a test"]] diff --git a/test/unit/test_config.py b/test/unit/test_config.py deleted file mode 100644 index ed84ce0..0000000 --- a/test/unit/test_config.py +++ /dev/null @@ -1,132 +0,0 @@ -import os -from shutil import copyfile -from tempfile import TemporaryDirectory - -import pytest -import yaml - -from ozy import OzyError -from ozy.config import safe_expand, parse_ozy_conf, resolve, apply_overrides, get_user_conf_file, load_config, get_system_variables - - -def test_parse_ozy_conf(): - sample_config_file = "conf/sample-team-conf.yaml" - ozy_conf = parse_ozy_conf(sample_config_file) - assert ozy_conf is not None - assert 'name' in ozy_conf - assert 'ozy_version' in ozy_conf - assert 'templates' in ozy_conf - assert 'apps' in ozy_conf - - templates = ozy_conf['templates'] - assert 'hashicorp' in templates - - -def test_safe_expand(): - sample_config = { - "terraform": { - "version": "0.12.10", - "url": "https://releases.hashicorp.com/terraform/{version}/terraform_{version}_{hashicorp_os}_amd64.zip", - "list": ['foo', '{version}', 'bar'] - }, - } - - tool_info = sample_config['terraform'] - expanded_config = safe_expand(dict(version="0.12.10", hashicorp_os="linux"), tool_info['url']) - assert expanded_config == "https://releases.hashicorp.com/terraform/0.12.10/terraform_0.12.10_linux_amd64.zip" - expanded_config = safe_expand(dict(version="0.12.10", hashicorp_os="linux"), tool_info['list']) - assert expanded_config == ['foo', '0.12.10', 'bar'] - - -def test_bad_safe_expand(): - with pytest.raises(OzyError): - safe_expand(dict(foo="bar"), "I am templated {baz}") - - -def test_resolve(): - sample_config_file = "conf/sample-team-conf.yaml" - ozy_conf = parse_ozy_conf(sample_config_file) - templates = ozy_conf['templates'] - config = ozy_conf['apps']['nomad'] - - with pytest.raises(OzyError): - bad_templates = templates.copy() - del bad_templates['hashicorp'] - resolve(config, bad_templates) - - # with a version check turned on, the below 3 LOC will verify that flipping config and template is caught. - # try intentionally switching them - # with pytest.raises(OzyError): - # resolve(config=templates, templates=config) - - good_resolved = resolve(config, templates) - assert 'url' in good_resolved - assert 'template' in good_resolved - assert 'type' in good_resolved - assert 'app_name' in good_resolved - assert 'version' in good_resolved - assert 'sha256' in good_resolved - assert 'sha256_gpg_key' in good_resolved - - -def test_apply_overrides(): - # test the flat case - source = {'baz': 'super-rab'} - dest = {'baz': 'rab'} - result = apply_overrides(source, dest) - assert result['baz'] == 'super-rab' - - # test the nested dict case - source = {'foo': {'bar': 'super-baz'}} - dest = {'foo': {'bar': 'baz'}} - result = apply_overrides(source, dest) - assert result['foo']['bar'] == 'super-baz' - - -def save_temp_ozy_conf(config, target_path): - with open(target_path, "w") as fobj: - yaml.dump(config, fobj) - - -def test_load_config(): - with TemporaryDirectory() as td: - subdirA = os.path.join(td, "A") - subdirA1 = os.path.join(subdirA, "1") - - os.mkdir(subdirA) - os.mkdir(subdirA1) - - sample_config = 'conf/sample-team-conf.yaml' - subdirA_config = parse_ozy_conf(sample_config) - subdirA_config['apps']['nomad']['version'] = '10.10.10.10' - save_temp_ozy_conf(subdirA_config, os.path.join(subdirA, ".ozy.yaml")) - - loaded_config = load_config(config=parse_ozy_conf(sample_config), - current_working_directory=subdirA) - - assert loaded_config['apps']['nomad']['version'] == '10.10.10.10' - - # lets try with three dirs! - subdirA1_config = parse_ozy_conf(sample_config) - subdirA1_config['apps']['nomad']['version'] = '11.11.11.11' - save_temp_ozy_conf(subdirA1_config, os.path.join(subdirA1, ".ozy.yaml")) - - loaded_config = load_config(config=parse_ozy_conf(sample_config), - current_working_directory=subdirA1) - - assert loaded_config['apps']['nomad']['version'] == '11.11.11.11' - - -def test_get_user_conf_file(): - ucf = get_user_conf_file() - assert ucf - - -def test_system_variables(): - system_variables = get_system_variables() - assert 'ozy_os' in system_variables - assert 'ozy_machine' in system_variables - assert 'ozy_arch' in system_variables - - expanded_config = safe_expand(dict(), "{ozy_os} {ozy_machine} {ozy_arch}") - assert expanded_config == f"{os.uname().sysname.lower()} {os.uname().machine} {'amd64' if os.uname().machine == 'x86_64' else os.uname().machine}" diff --git a/test/unit/test_installer.py b/test/unit/test_installer.py deleted file mode 100644 index 2e1713e..0000000 --- a/test/unit/test_installer.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - -from ozy import OzyError -from ozy.installer import Installer - - -def test_empty_dict_is_ok(): - Installer('test', dict()) - - -def test_drops_non_specified_keys(): - installer = Installer('test', dict(not_mentioned='123')) - with pytest.raises(KeyError): - installer.config('not_mentioned') - - -def test_accepts_non_required_keys(): - installer = Installer('test', dict(optional_key='opt'), optional_key='some default') - assert installer.config('optional_key') == 'opt' - - -def test_defaults_non_required_keys(): - installer = Installer('test', dict(), optional_key='some default') - assert installer.config('optional_key') == 'some default' - - -def test_accepts_required_keys(): - installer = Installer('test', dict(version='1.2.3'), 'version') - assert installer.config('version') == '1.2.3' - - -def test_throws_on_missing_required_keys(): - with pytest.raises(OzyError, match='Missing required key.*not_there.*in.*test.*'): - Installer('test', dict(), 'not_there') diff --git a/test/unit/test_ozy.py b/test/unit/test_ozy.py deleted file mode 100644 index 9f50750..0000000 --- a/test/unit/test_ozy.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -import pytest - -from ozy import OzyError -from ozy.files import walk_up_dirs, get_ozy_dir - - -def test_ozy_dirs(): - ozy_dir = get_ozy_dir() - assert ozy_dir is not None - home = os.environ['HOME'] - del os.environ['HOME'] - with pytest.raises(OzyError): - get_ozy_dir() - os.environ['HOME'] = home - - -def test_walk_up_dirs(): - test_path = os.path.join(os.path.sep, 'one', 'two', 'three') - assert [ - os.path.join(os.path.sep, 'one', 'two', 'three'), - os.path.join(os.path.sep, 'one', 'two'), - os.path.join(os.path.sep, 'one'), - os.path.sep - ] == [x for x in walk_up_dirs(test_path)] diff --git a/test/unit/test_utils.py b/test/unit/test_utils.py deleted file mode 100644 index d4b8d9e..0000000 --- a/test/unit/test_utils.py +++ /dev/null @@ -1,21 +0,0 @@ -from ozy.utils import restore_overridden_env_vars - - -def test_restore_original_env_vars_keeps_existing(): - orig = dict(a='a', b='b', c='c') - assert restore_overridden_env_vars(orig) == orig - - -def test_restore_original_env_vars_restores_pythonpath(): - orig = dict(PYTHONPATH='new', PYTHONPATH_ORIG='orig') - assert restore_overridden_env_vars(orig) == dict(PYTHONPATH='orig') - - -def test_restore_original_env_vars_restores_ld_library_path(): - orig = dict(LD_LIBRARY_PATH='new', LD_LIBRARY_PATH_ORIG='orig') - assert restore_overridden_env_vars(orig) == dict(LD_LIBRARY_PATH='orig') - - -def test_restore_original_env_vars_unsets_pythonpath_and_ld_path_if_not_originally_set(): - orig = dict(PYTHONPATH='new', LD_LIBRARY_PATH='new') - assert restore_overridden_env_vars(orig) == dict() diff --git a/rozy/test_resource/unittest.ozy.yaml b/test_resource/unittest.ozy.yaml similarity index 100% rename from rozy/test_resource/unittest.ozy.yaml rename to test_resource/unittest.ozy.yaml