From 91b0721ce6ba2a9a5de384d599e978dfbdb74721 Mon Sep 17 00:00:00 2001 From: Kevin Meinhardt Date: Wed, 18 Dec 2024 23:35:26 +0100 Subject: [PATCH] Split CI dependencies and add ./deps:/deps mount. (#22934) * Test checkout remote * Fix broken checks --- .dockerignore | 2 +- .github/actions/build-docker/action.yml | 6 +- .github/actions/run-docker/action.yml | 14 +- .github/workflows/_test.yml | 1 + .github/workflows/_test_check.yml | 23 ++- .github/workflows/_test_main.yml | 1 + .github/workflows/ci.yml | 6 +- .gitignore | 3 +- Dockerfile | 53 ++----- Makefile-docker | 20 ++- Makefile-os | 12 +- deps/.gitkeep | 0 docker-compose.yml | 3 + scripts/install_deps.py | 94 ++++++++++++ scripts/setup.py | 6 +- settings.py | 3 +- src/olympia/amo/monitors.py | 1 + src/olympia/amo/tests/test_monitor.py | 14 ++ src/olympia/core/apps.py | 4 +- src/olympia/core/tests/test_apps.py | 18 +++ tests/__init__.py | 6 + tests/make/test_install_deps.py | 181 ++++++++++++++++++++++++ tests/make/test_setup.py | 37 ++++- 23 files changed, 437 insertions(+), 71 deletions(-) create mode 100644 deps/.gitkeep create mode 100755 scripts/install_deps.py create mode 100644 tests/make/test_install_deps.py diff --git a/.dockerignore b/.dockerignore index 10e1a2d62782..e7b5b4c3d5aa 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,7 +28,7 @@ backups build*.py buildx-bake-metadata.json -deps +deps/* docker*.yml docker/artifacts/* docs/_build diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 98c71da8aca1..b5b77025cf5f 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -10,9 +10,6 @@ inputs: version: required: true description: The image version to tag with - target: - required: true - description: The stage to target in the build push: required: false description: Push the image? @@ -57,7 +54,8 @@ runs: - name: Create .env and version.json files shell: bash run: | - echo "DOCKER_TARGET=${{ inputs.target }}" >> $GITHUB_ENV + # We only build the production image in CI + echo "DOCKER_TARGET=production" >> $GITHUB_ENV echo "DOCKER_VERSION=${{ steps.meta.outputs.version }}" >> $GITHUB_ENV echo "DOCKER_COMMIT=${{ steps.context.outputs.git_sha }}" >> $GITHUB_ENV echo "DOCKER_BUILD=${{ steps.context.outputs.git_build_url }}" >> $GITHUB_ENV diff --git a/.github/actions/run-docker/action.yml b/.github/actions/run-docker/action.yml index 5a826110115a..1619f498eb95 100644 --- a/.github/actions/run-docker/action.yml +++ b/.github/actions/run-docker/action.yml @@ -19,8 +19,16 @@ inputs: description: 'Skip data backup' required: false default: 'true' + target: + description: 'Docker target to run (development|production)' + required: false + default: 'production' mount: - description: 'Mount olympia files from host' + description: 'Mount host files at runtime? (development|production)' + required: false + default: 'production' + deps: + description: 'Which dependencies to install at runtime? (development|production)' required: false default: 'production' @@ -34,8 +42,10 @@ runs: make up \ DOCKER_VERSION="${{ inputs.version }}" \ DOCKER_DIGEST="${{ inputs.digest }}" \ + DOCKER_TARGET="${{ inputs.target }}" \ OLYMPIA_UID="$(id -u)" \ OLYMPIA_MOUNT="${{ inputs.mount }}" \ + OLYMPIA_DEPS="${{ inputs.deps }}" \ DATA_BACKUP_SKIP="${{ inputs.data_backup_skip }}" \ DOCKER_WAIT="true" @@ -47,6 +57,6 @@ runs: EOF - name: Logs - shell: bash if: ${{ inputs.logs }} + shell: bash run: docker compose logs diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 80da00809c98..81ed82c564e9 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -68,4 +68,5 @@ jobs: version: ${{ inputs.version }} digest: ${{ inputs.digest }} services: ${{ matrix.services }} + deps: development run: ${{ matrix.run }} diff --git a/.github/workflows/_test_check.yml b/.github/workflows/_test_check.yml index 50e634405ffd..eebd0f900289 100644 --- a/.github/workflows/_test_check.yml +++ b/.github/workflows/_test_check.yml @@ -45,16 +45,24 @@ jobs: runs-on: ubuntu-latest name: | version: '${{ matrix.version }}' | - mount: '${{ matrix.mount }}' + target: '${{ matrix.target }}' | + mount: '${{ matrix.mount }}' | + deps: '${{ matrix.deps }}' strategy: fail-fast: false matrix: version: - local - ${{ inputs.version }} + target: + - development + - production mount: - development - production + deps: + - development + - production steps: - uses: actions/checkout@v4 - shell: bash @@ -63,7 +71,9 @@ jobs: cat < settings_local.py DJANGO_SETTINGS_MODULE="settings_local" make -f Makefile-docker update_assets EOF -FROM base AS sources - - - -# Add our custom mime types (required for for ts/json/md files) -COPY docker/etc/mime.types /etc/mime.types +FROM base AS production # Copy the rest of the source files from the host COPY --chown=olympia:olympia . ${HOME} +# Copy compiled locales from builder +COPY --from=locales --chown=olympia:olympia ${HOME}/locale ${HOME}/locale # Copy assets from assets COPY --from=assets --chown=olympia:olympia ${HOME}/site-static ${HOME}/site-static COPY --from=assets --chown=olympia:olympia ${HOME}/static-build ${HOME}/static-build +# Copy build info from info COPY --from=info ${BUILD_INFO} ${BUILD_INFO} - -# Set shell back to sh until we can prove we can use bash at runtime -SHELL ["/bin/sh", "-c"] - -FROM sources AS development - -# Copy dependencies from `pip_development` -COPY --from=pip_development --chown=olympia:olympia /deps /deps - -FROM sources AS production - # Copy compiled locales from builder COPY --from=locales --chown=olympia:olympia ${HOME}/locale ${HOME}/locale # Copy dependencies from `pip_production` COPY --from=pip_production --chown=olympia:olympia /deps /deps - - diff --git a/Makefile-docker b/Makefile-docker index 00d3cee0ba71..87c2ef8c6bc2 100644 --- a/Makefile-docker +++ b/Makefile-docker @@ -18,6 +18,13 @@ REQUIRED_FILES := \ /deps/package-lock.json \ /addons-server-docker-container \ +# Build list of dependencies to install +DEPS = pip prod +# If we're running a development image, then we should install the development dependencies +ifeq ($(OLYMPIA_DEPS), development) +DEPS += dev +endif + .PHONY: help_redirect help_redirect: @$(MAKE) help --no-print-directory @@ -28,12 +35,9 @@ check_debian_packages: ## check the existence of multiple debian packages .PHONY: check_pip_packages check_pip_packages: ## check the existence of multiple python packages - @ ./scripts/check_pip_packages.sh prod.txt -# "production" corresponds to the "propduction" DOCKER_TARGET defined in the Dockerfile -# When the target is "production" it means we cannot expect dev.txt dependencies to be installed. - @if [ "$(DOCKER_TARGET)" != "production" ]; then \ - ./scripts/check_pip_packages.sh dev.txt; \ - fi + @for dep in $(DEPS); do \ + ./scripts/check_pip_packages.sh $$dep.txt; \ + done .PHONY: check_files check_files: ## check the existence of multiple files @@ -77,6 +81,10 @@ update_assets: $(PYTHON_COMMAND) manage.py collectstatic --noinput +.PHONY: update_deps +update_deps: ## Update the dependencies + $(HOME)/scripts/install_deps.py $(DEPS) + # TOOD: remove this after we migrate addons-frontned to not depend on it. .PHONY: setup-ui-tests setup-ui-tests: diff --git a/Makefile-os b/Makefile-os index 98c7e6d89085..e3079503fcd3 100644 --- a/Makefile-os +++ b/Makefile-os @@ -58,7 +58,6 @@ CLEAN_PATHS := \ version.json \ logs \ buildx-bake-metadata.json \ - deps \ .PHONY: help_redirect help_redirect: @@ -154,8 +153,17 @@ docker_clean_build_cache: ## Remove buildx build cache .PHONY: clean_docker clean_docker: docker_compose_down docker_mysqld_volume_remove docker_clean_images docker_clean_volumes docker_clean_build_cache ## Remove all docker resources taking space on the host machine +.PHONY: docker_update_deps +docker_update_deps: docker_mysqld_volume_create ## Update the dependencies in the container based on the docker tag and target + docker compose run \ + --rm \ + --no-deps \ + $(DOCKER_RUN_ARGS) \ + web \ + make update_deps + .PHONY: up_pre -up_pre: setup docker_pull_or_build ## Pre-up the environment, setup files, volumes and host state +up_pre: setup docker_pull_or_build docker_update_deps ## Pre-up the environment, setup files, volumes and host state .PHONY: up_start up_start: docker_mysqld_volume_create ## Start the docker containers diff --git a/deps/.gitkeep b/deps/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docker-compose.yml b/docker-compose.yml index e8601a7d121e..4e847cacc989 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,6 +46,7 @@ services: volumes: # used by: web, worker, nginx - ${HOST_MOUNT_SOURCE:?}:/data/olympia + - ${HOST_MOUNT_SOURCE:?}deps:/deps - data_site_static:/data/olympia/site-static - ${HOST_MOUNT_SOURCE:?}storage:/data/olympia/storage worker: @@ -63,6 +64,7 @@ services: ] volumes: - ${HOST_MOUNT_SOURCE:?}:/data/olympia + - ${HOST_MOUNT_SOURCE:?}deps:/deps - ${HOST_MOUNT_SOURCE:?}storage:/data/olympia/storage extra_hosts: - "olympia.test:127.0.0.1" @@ -215,6 +217,7 @@ volumes: # it will map to the current directory ./ # (data_olympia_):/ data_olympia_: + data_olympia_deps: data_olympia_storage: # Volume for rabbitmq/redis to avoid anonymous volumes data_rabbitmq: diff --git a/scripts/install_deps.py b/scripts/install_deps.py new file mode 100755 index 000000000000..5ef16dbbfb82 --- /dev/null +++ b/scripts/install_deps.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +import os +import shutil +import subprocess +import sys + + +def copy_package_json(): + """Copy package.json files to deps directory if they exist.""" + try: + shutil.copy('/data/olympia/package.json', '/deps') + shutil.copy('/data/olympia/package-lock.json', '/deps') + except (IOError, OSError): + pass # Ignore if files don't exist or can't be copied + + +def main(targets): + # Constants + ALLOWED_NPM_TARGETS = set(['prod', 'dev']) + DOCKER_TAG = os.environ.get('DOCKER_TAG', 'local') + DOCKER_TARGET = os.environ.get('DOCKER_TARGET', '') + OLYMPIA_DEPS = os.environ.get('OLYMPIA_DEPS', '') + + if not targets: + raise ValueError('No targets specified') + + print( + 'Updating deps... \n', + f'targets: {", ".join(targets)} \n', + f'DOCKER_TAG: {DOCKER_TAG} \n', + f'DOCKER_TARGET: {DOCKER_TARGET} \n', + f'OLYMPIA_DEPS: {OLYMPIA_DEPS} \n', + ) + + # If we are installing production dependencies or on a non local image + # we always remove existing deps as we don't know what was previously + # installed or in the host ./deps directory before running this script + if 'local' not in DOCKER_TAG or OLYMPIA_DEPS == 'production': + print('Removing existing deps') + for item in os.listdir('/deps'): + item_path = os.path.join('/deps', item) + if os.path.isdir(item_path) and item != 'cache': + shutil.rmtree(item_path) + else: + print('Updating existing deps') + + # Copy package.json files + copy_package_json() + + # Prepare the includes lists + pip_includes = [] + npm_includes = [] + + # PIP_COMMAND is set by the Dockerfile + pip_command = os.environ['PIP_COMMAND'] + pip_args = pip_command.split() + [ + 'install', + '--progress-bar=off', + '--no-deps', + '--exists-action=w', + ] + + # NPM_ARGS is set by the Dockerfile + npm_args_env = os.environ['NPM_ARGS'] + npm_args = [ + 'npm', + 'install', + '--no-save', + '--no-audit', + '--no-fund', + ] + npm_args_env.split() + + # Add the relevant targets to the includes lists + for target in targets: + pip_includes.append(target) + pip_args.extend(['-r', f'requirements/{target}.txt']) + if target in ALLOWED_NPM_TARGETS: + npm_includes.append(target) + npm_args.extend(['--include', target]) + + if pip_includes: + # Install pip dependencies + print(f"Installing pip dependencies: {', '.join(pip_includes)} \n") + subprocess.run(pip_args, check=True) + + if npm_includes: + # Install npm dependencies + print(f"Installing npm dependencies: {', '.join(npm_includes)} \n") + subprocess.run(npm_args, check=True) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/scripts/setup.py b/scripts/setup.py index f8c6111d07eb..176a05237c51 100755 --- a/scripts/setup.py +++ b/scripts/setup.py @@ -118,10 +118,11 @@ def main(): olympia_uid = os.getuid() olympia_mount, olympia_mount_source = get_olympia_mount(docker_target) - # DEBUG is special, as we should allow the user to override it + # These variables are special, as we should allow the user to override them # but we should not set a default to the previously set value but instead - # to the most sensible default. + # use a value derived from other stable values. debug = os.environ.get('DEBUG', str(False if is_production else True)) + olympia_deps = os.environ.get('OLYMPIA_DEPS', docker_target) set_env_file( { @@ -138,6 +139,7 @@ def main(): # to use as the source of the /data/olympia volume 'HOST_MOUNT_SOURCE': olympia_mount_source, 'DEBUG': debug, + 'OLYMPIA_DEPS': olympia_deps, } ) diff --git a/settings.py b/settings.py index 5fa6b23a2ef0..ad6b13112338 100644 --- a/settings.py +++ b/settings.py @@ -62,7 +62,8 @@ def insert_debug_toolbar_middleware(middlewares): return tuple(ret_middleware) -if DEV_MODE: +# We can only add these dependencies if we have development dependencies +if os.environ.get('OLYMPIA_DEPS', '') == 'development': INSTALLED_APPS += ( 'debug_toolbar', 'dbbackup', diff --git a/src/olympia/amo/monitors.py b/src/olympia/amo/monitors.py index 82fe51ca0f0c..4cf7eb0f8fd3 100644 --- a/src/olympia/amo/monitors.py +++ b/src/olympia/amo/monitors.py @@ -134,6 +134,7 @@ def elastic(): status = 'ES is red' elastic_results = health except Exception: + status = 'Failed to connect to Elasticsearch' elastic_results = {'exception': traceback.format_exc()} return status, elastic_results diff --git a/src/olympia/amo/tests/test_monitor.py b/src/olympia/amo/tests/test_monitor.py index 510d55185bfd..279596c41c24 100644 --- a/src/olympia/amo/tests/test_monitor.py +++ b/src/olympia/amo/tests/test_monitor.py @@ -49,6 +49,20 @@ def test_elastic(self): status, elastic_result = monitors.elastic() assert status == '' + @patch('olympia.amo.monitors.get_es', side_effect=Exception('Connection error')) + def test_elastic_connection_error(self, _): + status, elastic_result = monitors.elastic() + assert status == 'Failed to connect to Elasticsearch' + assert 'Connection error' in elastic_result['exception'] + + def test_elastic_status_red(self): + mock_es = MagicMock() + mock_es.cluster.health.return_value = {'status': 'red'} + with patch('olympia.amo.monitors.get_es', return_value=mock_es): + status, elastic_result = monitors.elastic() + assert status == 'ES is red' + assert elastic_result == {'status': 'red'} + @patch('os.path.exists') @patch('os.access') def test_path(self, mock_exists, mock_access): diff --git a/src/olympia/core/apps.py b/src/olympia/core/apps.py index 2a32723f5214..879b30868248 100644 --- a/src/olympia/core/apps.py +++ b/src/olympia/core/apps.py @@ -3,6 +3,7 @@ import subprocess import warnings from io import StringIO +from pwd import getpwnam from django.apps import AppConfig from django.conf import settings @@ -48,7 +49,8 @@ def host_check(app_configs, **kwargs): # set the expected uid to 9500, otherwise we expect the uid # passed to the environment to be the expected uid. expected_uid = 9500 if settings.HOST_UID is None else int(settings.HOST_UID) - actual_uid = os.getuid() + # Get the actual uid from the olympia user + actual_uid = getpwnam('olympia').pw_uid if actual_uid != expected_uid: return [ diff --git a/src/olympia/core/tests/test_apps.py b/src/olympia/core/tests/test_apps.py index 8264c125e7e4..bb4c8b6d1ffe 100644 --- a/src/olympia/core/tests/test_apps.py +++ b/src/olympia/core/tests/test_apps.py @@ -3,6 +3,7 @@ from django.core.management import call_command from django.core.management.base import SystemCheckError from django.test import TestCase +from django.test.utils import override_settings from olympia.core.utils import REQUIRED_VERSION_KEYS @@ -67,3 +68,20 @@ def test_missing_version_keys_check(self): f'{broken_key} is missing from version.json', ): call_command('check') + + @override_settings(HOST_UID=None) + @mock.patch('olympia.core.apps.getpwnam') + def test_illegal_override_uid_check(self, mock_getpwnam): + """ + In production, or when HOST_UID is not set, we expect to not override + the default uid of 9500 for the olympia user. + """ + mock_getpwnam.return_value.pw_uid = 1000 + with self.assertRaisesMessage( + SystemCheckError, + 'Expected user uid to be 9500', + ): + call_command('check') + + with override_settings(HOST_UID=1000): + call_command('check') diff --git a/tests/__init__.py b/tests/__init__.py index e69de29bb2d1..1376445013ed 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,6 @@ +import os +from unittest import mock + + +def override_env(**kwargs): + return mock.patch.dict(os.environ, kwargs, clear=True) diff --git a/tests/make/test_install_deps.py b/tests/make/test_install_deps.py new file mode 100644 index 000000000000..2a4d6344b24a --- /dev/null +++ b/tests/make/test_install_deps.py @@ -0,0 +1,181 @@ +import unittest +from unittest import mock + +from scripts.install_deps import copy_package_json, main +from tests import override_env + + +@mock.patch('scripts.install_deps.shutil.copy') +class TestCopyPackageJson(unittest.TestCase): + def test_copy_package_json(self, mock_shutil): + copy_package_json() + assert mock_shutil.call_args_list == [ + mock.call('/data/olympia/package.json', '/deps'), + mock.call('/data/olympia/package-lock.json', '/deps'), + ] + + def test_copy_package_json_no_files(self, mock_shutil): + mock_shutil.side_effect = IOError + copy_package_json() + assert mock_shutil.call_args_list == [ + mock.call('/data/olympia/package.json', '/deps'), + ] + + +class TestInstallDeps(unittest.TestCase): + def setUp(self): + mocks = ['shutil.rmtree', 'os.listdir', 'subprocess.run', 'copy_package_json'] + self.mocks = {} + for mock_name in mocks: + patch = mock.patch( + f'scripts.install_deps.{mock_name}', + ) + self.mocks[mock_name] = patch.start() + self.addCleanup(patch.stop) + + def test_raises_no_targets(self): + """ + Test that the function raises a ValueError if + no or invalid targets are specified + """ + for argument in [None, '', []]: + with self.assertRaises(ValueError): + main(argument) + + def _test_remove_existing_deps(self, args, expect_remove=False): + self.mocks['os.listdir'].return_value = [ + 'cache', + 'lib', + 'node_modules', + 'package.json', + 'package-lock.json', + ] + with override_env( + **{ + 'PIP_COMMAND': 'pip-test', + 'NPM_ARGS': 'npm-test', + **args, + } + ): + main(['prod']) + + if expect_remove: + assert self.mocks['os.listdir'].called + assert self.mocks['shutil.rmtree'].call_args_list == [ + mock.call('/deps/lib'), + mock.call('/deps/node_modules'), + ] + else: + assert not self.mocks['os.listdir'].called + assert not self.mocks['shutil.rmtree'].called + + def test_keep_deps_for_local_mixed_env(self): + """Test that dependencies are kept when running + locally with development deps in production""" + args = { + 'DOCKER_TAG': 'local', + 'OLYMPIA_DEPS': 'development', + 'DOCKER_TARGET': 'production', + } + self._test_remove_existing_deps(args, expect_remove=False) + + def test_keep_deps_for_local_default(self): + """Test that dependencies are kept when running locally with default settings""" + args = {'DOCKER_TAG': 'local', 'OLYMPIA_DEPS': 'development'} + self._test_remove_existing_deps(args, expect_remove=False) + + def test_remove_deps_for_non_local(self): + """Test that dependencies are removed when running in production environment""" + args = {'DOCKER_TAG': 'prod'} + self._test_remove_existing_deps(args, expect_remove=True) + + def test_remove_deps_for_prod_deps_in_dev(self): + """Test that dependencies are removed when + installing production deps in development""" + args = { + 'DOCKER_TAG': 'local', + 'OLYMPIA_DEPS': 'production', + 'DOCKER_TARGET': 'development', + } + self._test_remove_existing_deps(args, expect_remove=True) + + def test_remove_deps_for_prod_deps_in_prod(self): + """Test that dependencies are removed when + installing production deps in production""" + args = { + 'DOCKER_TAG': 'local', + 'OLYMPIA_DEPS': 'production', + 'DOCKER_TARGET': 'production', + } + self._test_remove_existing_deps(args, expect_remove=True) + + def test_copy_package_json_called(self): + """Test that copy_package_json is called""" + main(['prod']) + assert self.mocks['copy_package_json'].called + + @override_env(PIP_COMMAND='pip-test', NPM_ARGS='npm-test') + def test_pip_command_set_on_environment(self): + main(['prod']) + assert self.mocks['subprocess.run'].call_args_list[0][0][0][0] == 'pip-test' + + @override_env() + def test_pip_command_not_set_on_environment(self): + self.assertRaises(KeyError, main, ['prod']) + + @override_env(NPM_ARGS='npm-test', PIP_COMMAND='pip-test') + def test_npm_command_set_on_environment(self): + main(['prod']) + assert 'npm-test' in self.mocks['subprocess.run'].call_args_list[1][0][0] + + @override_env() + def test_npm_command_not_set_on_environment(self): + self.assertRaises(KeyError, main, ['prod']) + + def test_correct_args_passed_to_subprocesses(self): + """ + Test that the correct arguments are passed to the subprocesses + """ + main(['pip', 'prod', 'dev']) + + assert self.mocks['subprocess.run'].call_args_list == [ + mock.call( + [ + 'python3', + '-m', + 'pip', + 'install', + '--progress-bar=off', + '--no-deps', + '--exists-action=w', + '-r', + 'requirements/pip.txt', + '-r', + 'requirements/prod.txt', + '-r', + 'requirements/dev.txt', + ], + check=True, + ), + # NPM excludes the pip target + mock.call( + [ + 'npm', + 'install', + '--no-save', + '--no-audit', + '--no-fund', + '--prefix', + '/deps/', + '--cache', + '/deps/cache/npm', + '--loglevel', + 'verbose', + '--include', + 'prod', + '--include', + 'dev', + ], + check=True, + ), + ] diff --git a/tests/make/test_setup.py b/tests/make/test_setup.py index a72037b331c2..6656f64d66e3 100644 --- a/tests/make/test_setup.py +++ b/tests/make/test_setup.py @@ -3,10 +3,7 @@ from unittest import mock from scripts.setup import get_docker_tag, main - - -def override_env(**kwargs): - return mock.patch.dict(os.environ, kwargs, clear=True) +from tests import override_env keys = [ @@ -15,6 +12,7 @@ def override_env(**kwargs): 'HOST_UID', 'HOST_MOUNT', 'HOST_MOUNT_SOURCE', + 'OLYMPIA_DEPS', 'DEBUG', ] @@ -109,7 +107,7 @@ def test_default_when_version_is_empty(self): self.assertEqual(version, 'local') self.assertEqual(digest, None) - @override_env(DOCKER_DIGEST='') + @override_env(DOCKER_DIGEST='', DOCKER_TAG='image@sha256:123') def test_default_when_digest_is_empty(self): self.mock_get_env_file.return_value = {'DOCKER_TAG': 'image@sha256:123'} tag, version, digest = get_docker_tag() @@ -202,3 +200,32 @@ def test_host_mount_from_file_ignored(self): self.assert_set_env_file_called_with( HOST_MOUNT='production', HOST_MOUNT_SOURCE='data_olympia_' ) + + +@override_env() +class TestOlympiaDeps(BaseTestClass): + def test_default_olympia_deps(self): + main() + self.assert_set_env_file_called_with(OLYMPIA_DEPS='development') + + @override_env(DOCKER_TARGET='production') + def test_production_olympia_deps(self): + main() + self.assert_set_env_file_called_with(OLYMPIA_DEPS='production') + + @override_env(DOCKER_TARGET='production') + def test_override_env_olympia_deps_development_on_target_production(self): + self.mock_get_env_file.return_value = {'OLYMPIA_DEPS': 'development'} + main() + self.assert_set_env_file_called_with(OLYMPIA_DEPS='production') + + @override_env(DOCKER_TARGET='development') + def test_override_env_olympia_deps_development_on_target_development(self): + self.mock_get_env_file.return_value = {'OLYMPIA_DEPS': 'production'} + main() + self.assert_set_env_file_called_with(OLYMPIA_DEPS='development') + + @override_env(OLYMPIA_DEPS='test') + def test_olympia_deps_override(self): + main() + self.assert_set_env_file_called_with(OLYMPIA_DEPS='test')