Skip to content

Commit

Permalink
Include DOCKER_* build info statically in image
Browse files Browse the repository at this point in the history
- Modified `Dockerfile` to expose build arguments and create a static build info file, ensuring that build variables are hard-coded and not overridden at runtime.
- Refactored `get_version_json` function in `utils.py` to read build information from the newly created static file, ensuring required keys are validated.
- Updated tests in `test_apps.py` and `test_utils.py` to check for required version keys and validate the build info retrieval process.
  • Loading branch information
KevinMind committed Dec 9, 2024
1 parent b619b89 commit ea0dc7b
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 116 deletions.
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,28 @@ RUN localedef -i en_US -f UTF-8 en_US.UTF-8
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8

# Build args that determine the tag and stage of the build
# These are passed to docker via the bake.hcl file
ARG DOCKER_COMMIT
ARG DOCKER_VERSION
ARG DOCKER_BUILD
ARG DOCKER_TARGET

# Expose these variables via hard coded file to prevent them from being
# overridden by the environment when running the container. These
# values represent the build and should not be changable afterwards.
ENV BUILD_INFO=/build-info
RUN <<EOF
# Create the build file hard coding build variables to the image
cat <<INNEREOF > ${BUILD_INFO}
commit="${DOCKER_COMMIT}"
version="${DOCKER_VERSION}"
build="${DOCKER_BUILD}"
target="${DOCKER_TARGET}"
INNEREOF
# Set permissions to make the file readable by all but only writable by root
chmod 644 ${BUILD_INFO}

# Create directory for dependencies
mkdir /deps
chown -R olympia:olympia /deps
Expand Down
4 changes: 4 additions & 0 deletions Makefile-os
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
DOCKER_PROGRESS ?= auto
DOCKER_METADATA_FILE ?= buildx-bake-metadata.json
DOCKER_PUSH ?=
# Values that are not saved to .env
# but should be set in the docker image
# default to static values to prevent
# invalidating docker build cache
export DOCKER_COMMIT ?=
export DOCKER_BUILD ?=
export DOCKER_VERSION ?=
Expand Down
8 changes: 4 additions & 4 deletions docker-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ target "web" {
tags = ["${DOCKER_TAG}"]
platforms = ["linux/amd64"]
args = {
DOCKER_COMMIT = "${DOCKER_COMMIT}"
DOCKER_VERSION = "${DOCKER_VERSION}"
DOCKER_BUILD = "${DOCKER_BUILD}"
DOCKER_TARGET = "${DOCKER_TARGET}"
DOCKER_COMMIT = "${DOCKER_COMMIT}"
DOCKER_VERSION = "${DOCKER_VERSION}"
DOCKER_BUILD = "${DOCKER_BUILD}"
DOCKER_TARGET = "${DOCKER_TARGET}"
}
pull = true

Expand Down
13 changes: 8 additions & 5 deletions src/olympia/core/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
from django.db import connection
from django.utils.translation import gettext_lazy as _

from olympia.core.utils import get_version_json
from olympia.core.utils import (
REQUIRED_VERSION_KEYS,
get_version_json,
)


log = logging.getLogger('z.startup')
Expand Down Expand Up @@ -64,11 +67,9 @@ def host_check(app_configs, **kwargs):
@register(CustomTags.custom_setup)
def version_check(app_configs, **kwargs):
"""Check the (virtual) version.json file exists and has the correct keys."""
required_keys = ['version', 'build', 'commit', 'source']

version = get_version_json()

missing_keys = [key for key in required_keys if key not in version]
missing_keys = [key for key in REQUIRED_VERSION_KEYS if key not in version]

if missing_keys:
return [
Expand All @@ -85,8 +86,10 @@ def version_check(app_configs, **kwargs):
def static_check(app_configs, **kwargs):
errors = []
output = StringIO()
version = get_version_json()

if settings.DEV_MODE:
# We only run this check in production images.
if version.get('target') != 'production':
return []

try:
Expand Down
127 changes: 83 additions & 44 deletions src/olympia/core/tests/test_apps.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,30 @@
import os
from unittest import mock

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

class SystemCheckIntegrationTest(TestCase):
@mock.patch('olympia.core.apps.os.getuid')
def test_illegal_override_uid_check(self, mock_getuid):
"""
If the HOST_UID is not set or if it is not set to the
olympia user actual uid, then file ownership is probably
incorrect and we should fail the check.
"""
dummy_uid = '1000'
olympia_uid = '9500'
for host_uid in [None, olympia_uid]:
with override_settings(HOST_UID=host_uid):
with self.subTest(host_uid=host_uid):
mock_getuid.return_value = int(dummy_uid)
with self.assertRaisesMessage(
SystemCheckError,
f'Expected user uid to be {olympia_uid}, received {dummy_uid}',
):
call_command('check')

with override_settings(HOST_UID=dummy_uid):
mock_getuid.return_value = int(olympia_uid)
with self.assertRaisesMessage(
SystemCheckError,
f'Expected user uid to be {dummy_uid}, received {olympia_uid}',
):
call_command('check')
class SystemCheckIntegrationTest(TestCase):
def setUp(self):
self.default_version_json = {
'tag': 'mozilla/addons-server:1.0',
'target': 'production',
'commit': 'abc',
'version': '1.0',
'build': 'http://example.com/build',
'source': 'https://github.com/mozilla/addons-server',
}
patch = mock.patch(
'olympia.core.apps.get_version_json',
return_value=self.default_version_json,
)
self.mock_get_version_json = patch.start()
self.addCleanup(patch.stop)

@mock.patch('olympia.core.apps.connection.cursor')
def test_db_charset_check(self, mock_cursor):
Expand All @@ -56,29 +48,76 @@ def test_uwsgi_check(self):
):
call_command('check')

def test_version_missing_key(self):
call_command('check')

with mock.patch('olympia.core.apps.get_version_json') as get_version_json:
keys = ['version', 'build', 'commit', 'source']
version_mock = {key: 'test' for key in keys}

for key in keys:
version = version_mock.copy()
version.pop(key)
get_version_json.return_value = version

def test_missing_version_keys_check(self):
"""
We expect all required version keys to be set during the docker build.
"""
for broken_key in REQUIRED_VERSION_KEYS:
with self.subTest(broken_key=broken_key):
del self.mock_get_version_json.return_value[broken_key]
with self.assertRaisesMessage(
SystemCheckError, f'{key} is missing from version.json'
SystemCheckError,
f'Expected key: {broken_key} to exist',
):
call_command('check')

def test_version_missing_multiple_keys(self):
call_command('check')
def test_dockerignore_file_exists_check(self):
"""
In production, or when the host mount is set to production, we expect
not to find docker ignored files like Makefile-os in the file system.
"""
original_exists = os.path.exists

def mock_exists(path):
return path == '/data/olympia/Makefile-os' or original_exists(path)

for host_mount in (None, 'production'):
with self.subTest(host_mount=host_mount):
with override_settings(OLYMPIA_MOUNT=host_mount):
with mock.patch('os.path.exists', side_effect=mock_exists):
with self.assertRaisesMessage(
SystemCheckError,
'Makefile-os should be excluded by dockerignore',
):
call_command('check')

@override_settings(OLYMPIA_UID=None)
@mock.patch('olympia.core.apps.os.getuid')
def test_illegal_override_uid_check(self, mock_getuid):
"""
In production, or when OLYMPIA_UID is not set, we expect to not override
the default uid of 9500 for the olympia user.
"""
mock_getuid.return_value = 1000
with self.assertRaisesMessage(
SystemCheckError,
'Expected user uid to be 9500',
):
call_command('check')

with mock.patch('olympia.core.apps.get_version_json') as get_version_json:
get_version_json.return_value = {'version': 'test', 'build': 'test'}
@mock.patch('olympia.core.apps.os.getuid')
def test_illegal_override_uid_check(self, mock_getuid):
"""
If the HOST_UID is not set or if it is not set to the
olympia user actual uid, then file ownership is probably
incorrect and we should fail the check.
"""
dummy_uid = '1000'
olympia_uid = '9500'
for host_uid in [None, olympia_uid]:
with override_settings(HOST_UID=host_uid):
with self.subTest(host_uid=host_uid):
mock_getuid.return_value = int(dummy_uid)
with self.assertRaisesMessage(
SystemCheckError,
f'Expected user uid to be {olympia_uid}, received {dummy_uid}',
):
call_command('check')

with override_settings(HOST_UID=dummy_uid):
mock_getuid.return_value = int(olympia_uid)
with self.assertRaisesMessage(
SystemCheckError, 'commit, source is missing from version.json'
SystemCheckError,
f'Expected user uid to be {dummy_uid}, received {olympia_uid}',
):
call_command('check')
Loading

0 comments on commit ea0dc7b

Please sign in to comment.