Skip to content

Commit da1e227

Browse files
committed
Build: use new Docker images from design document
Minimal implementation for POC of #8447 It uses a Feature flag for now as a way to select the new `readthedocs/build:ubuntu20` image and install Python versions via `asdf`. MinIO requires a new bucket called `languages` with a pre-compiled Python 3.9.6 version to work (*) (this version is hardcoded for now). However, if a different version is selected it will be downloaded from official mirrors, installed and used. Build times on `latest` version for `test-build`: * using the new image + cached Python version: 112s * using the new image + non cached Python version: 288s * using old image (current production): 87s > Note that all the parsing of the Config File to support `build.os` and > `build.languages` is not included in this PR on purpose. That work can be > split as a separate work and done in parallel with the rest of work required > here. (*) to pre-compile a Python version: ```bash docker run -it readthedocs/build:ubuntu20 /bin/bash asdf install python 3.9.6 asdf global python 3.9.6 python -m pip install -U pip setuptools virtualenv cd /home/docs/.asdf/installs/python tar -cfvz ubuntu20-python-3.9.6.tar.gz 3.9.6 docker cp <container id>:/home/docs/.asdf/installs/python/ubuntu20-python-3.9.6.tar.gz . ``` and upload the .tar.gz file to MinIO `languages` bucket using the web interface
1 parent df0d7e2 commit da1e227

File tree

8 files changed

+138
-1
lines changed

8 files changed

+138
-1
lines changed

readthedocs/doc_builder/environments.py

+3
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,9 @@ def __init__(self, *args, **kwargs):
784784
# the image set by user or,
785785
if self.config and self.config.build.image:
786786
self.container_image = self.config.build.image
787+
# the new Docker image structure or,
788+
if self.project.has_feature(Feature.USE_NEW_DOCKER_IMAGES_STRUCTURE):
789+
self.container_image = 'readthedocs/build:ubuntu20'
787790
# the image overridden by the project (manually set by an admin).
788791
if self.project.container_image:
789792
self.container_image = self.project.container_image

readthedocs/doc_builder/python_environments.py

+105-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import logging
99
import os
1010
import shutil
11+
import tarfile
1112

1213
import yaml
1314
from django.conf import settings
@@ -22,6 +23,7 @@
2223
from readthedocs.doc_builder.loader import get_builder_class
2324
from readthedocs.projects.constants import LOG_TEMPLATE
2425
from readthedocs.projects.models import Feature
26+
from readthedocs.storage import build_languages_storage
2527

2628
log = logging.getLogger(__name__)
2729

@@ -317,15 +319,117 @@ def setup_base(self):
317319
cli_args.append(
318320
self.venv_path(),
319321
)
322+
323+
# TODO: make ``self.config.python_interpreter`` return the correct value
324+
if self.project.has_feature(Feature.USE_NEW_DOCKER_IMAGES_STRUCTURE):
325+
python_interpreter = 'python'
326+
else:
327+
python_interpreter = self.config.python_interpreter
328+
320329
self.build_env.run(
321-
self.config.python_interpreter,
330+
python_interpreter,
322331
*cli_args,
323332
# Don't use virtualenv bin that doesn't exist yet
324333
bin_path=None,
325334
# Don't use the project's root, some config files can interfere
326335
cwd=None,
327336
)
328337

338+
def install_languages(self):
339+
if settings.RTD_DOCKER_COMPOSE:
340+
# Create a symlink for ``root`` user to use the same ``.asdf``
341+
# installation than ``docs`` user. Required for local building
342+
# since everything is run as ``root`` when using Local Development
343+
# instance
344+
cmd = [
345+
'ln',
346+
'-s',
347+
'/home/docs/.asdf',
348+
'/root/.asdf',
349+
]
350+
self.build_env.run(
351+
*cmd,
352+
)
353+
354+
# TODO: do not use a Feature flag here, but check for ``build.os`` and
355+
# ``build.languages`` instead
356+
if self.project.has_feature(Feature.USE_NEW_DOCKER_IMAGES_STRUCTURE):
357+
# TODO: iterate over ``build.languages`` and install all languages
358+
# specified for this project
359+
os = 'ubuntu20'
360+
language = 'python'
361+
version = '3.9.6'
362+
363+
# TODO: generate the correct path for the Python version
364+
# language_path = f'{os}/{language}/2021-08-30/{version}.tar.gz'
365+
language_path = f'{os}-{language}-{version}.tar.gz'
366+
language_version_cached = build_languages_storage.exists(language_path)
367+
if language_version_cached:
368+
remote_fd = build_languages_storage.open(language_path, mode='rb')
369+
with tarfile.open(fileobj=remote_fd) as tar:
370+
# Extract it on the shared path between host and Docker container
371+
extract_path = os.path.join(self.project.doc_path, 'languages')
372+
tar.extractall(extra_path)
373+
374+
# Move the extracted content to the ``asdf`` installation
375+
cmd = [
376+
'mv',
377+
f'{extract_path}/{version}',
378+
f'/home/docs/.asdf/installs/{language}/{version}',
379+
]
380+
self.build_env.run(
381+
*cmd,
382+
)
383+
else:
384+
# If the language version selected is not available from the
385+
# cache we compile it at build time
386+
cmd = [
387+
'asdf',
388+
'install',
389+
language,
390+
version,
391+
]
392+
self.build_env.run(
393+
*cmd,
394+
)
395+
396+
# Make the language version chosen by the user the default one
397+
cmd = [
398+
'asdf',
399+
'global',
400+
language,
401+
version,
402+
]
403+
self.build_env.run(
404+
*cmd,
405+
)
406+
407+
# Recreate shims for this language to make the new version
408+
# installed available
409+
cmd = [
410+
'asdf',
411+
'reshim',
412+
language,
413+
]
414+
self.build_env.run(
415+
*cmd,
416+
)
417+
418+
if not language_version_cached:
419+
# Install our own requirements if the version is compiled
420+
cmd = [
421+
'python',
422+
'-m'
423+
'pip',
424+
'install',
425+
'-U',
426+
'virtualenv',
427+
'setuptools',
428+
]
429+
self.build_env.run(
430+
*cmd,
431+
)
432+
329433
def install_core_requirements(self):
330434
"""Install basic Read the Docs requirements into the virtualenv."""
331435
pip_install_cmd = [

readthedocs/projects/models.py

+5
Original file line numberDiff line numberDiff line change
@@ -1669,6 +1669,7 @@ def add_features(sender, **kwargs):
16691669
USE_SPHINX_BUILDERS = 'use_sphinx_builders'
16701670
DEDUPLICATE_BUILDS = 'deduplicate_builds'
16711671
DONT_CREATE_INDEX = 'dont_create_index'
1672+
USE_NEW_DOCKER_IMAGES_STRUCTURE = 'use_new_docker_images_structure'
16721673

16731674
FEATURES = (
16741675
(ALLOW_DEPRECATED_WEBHOOKS, _('Allow deprecated webhook views')),
@@ -1690,6 +1691,10 @@ def add_features(sender, **kwargs):
16901691
USE_TESTING_BUILD_IMAGE,
16911692
_('Use Docker image labelled as `testing` to build the docs'),
16921693
),
1694+
(
1695+
USE_NEW_DOCKER_IMAGES_STRUCTURE,
1696+
_('Use new Docker images that install languages at build time'),
1697+
),
16931698
(
16941699
API_LARGE_DATA,
16951700
_('Try alternative method of posting large data'),

readthedocs/projects/tasks.py

+1
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,7 @@ def update_app_instances(
11441144

11451145
def setup_build(self):
11461146
self.install_system_dependencies()
1147+
self.python_env.install_languages()
11471148
self.setup_python_environment()
11481149

11491150
def setup_python_environment(self):

readthedocs/settings/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ def USE_PROMOS(self): # noqa
310310
# https://docs.readthedocs.io/page/development/settings.html#rtd-build-media-storage
311311
RTD_BUILD_MEDIA_STORAGE = 'readthedocs.builds.storage.BuildMediaFileSystemStorage'
312312
RTD_BUILD_ENVIRONMENT_STORAGE = 'readthedocs.builds.storage.BuildMediaFileSystemStorage'
313+
RTD_BUILD_LANGUAGES_STORAGE = 'readthedocs.builds.storage.BuildMediaFileSystemStorage'
313314
RTD_BUILD_COMMANDS_STORAGE = 'readthedocs.builds.storage.BuildMediaFileSystemStorage'
314315

315316
@property

readthedocs/settings/docker_compose.py

+3
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ def show_debug_toolbar(request):
134134
RTD_BUILD_MEDIA_STORAGE = 'readthedocs.storage.s3_storage.S3BuildMediaStorage'
135135
# Storage backend for build cached environments
136136
RTD_BUILD_ENVIRONMENT_STORAGE = 'readthedocs.storage.s3_storage.S3BuildEnvironmentStorage'
137+
# Storage backend for build languages
138+
RTD_BUILD_LANGUAGES_STORAGE = 'readthedocs.storage.s3_storage.S3BuildLanguagesStorage'
137139
# Storage for static files (those collected with `collectstatic`)
138140
STATICFILES_STORAGE = 'readthedocs.storage.s3_storage.S3StaticStorage'
139141

@@ -142,6 +144,7 @@ def show_debug_toolbar(request):
142144
S3_MEDIA_STORAGE_BUCKET = 'media'
143145
S3_BUILD_COMMANDS_STORAGE_BUCKET = 'builds'
144146
S3_BUILD_ENVIRONMENT_STORAGE_BUCKET = 'envs'
147+
S3_BUILD_LANGUAGES_STORAGE_BUCKET = 'languages'
145148
S3_STATIC_STORAGE_BUCKET = 'static'
146149
S3_STATIC_STORAGE_OVERRIDE_HOSTNAME = 'community.dev.readthedocs.io'
147150
S3_MEDIA_STORAGE_OVERRIDE_HOSTNAME = 'community.dev.readthedocs.io'

readthedocs/storage/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ def _setup(self):
2626
self._wrapped = get_storage_class(settings.RTD_BUILD_COMMANDS_STORAGE)()
2727

2828

29+
class ConfiguredBuildLanguagesStorage(LazyObject):
30+
def _setup(self):
31+
self._wrapped = get_storage_class(settings.RTD_BUILD_LANGUAGES_STORAGE)()
32+
33+
2934
build_media_storage = ConfiguredBuildMediaStorage()
3035
build_environment_storage = ConfiguredBuildEnvironmentStorage()
3136
build_commands_storage = ConfiguredBuildCommandsStorage()
37+
build_languages_storage = ConfiguredBuildLanguagesStorage()

readthedocs/storage/s3_storage.py

+14
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,17 @@ def __init__(self, *args, **kwargs):
8989
'AWS S3 not configured correctly. '
9090
'Ensure S3_BUILD_ENVIRONMENT_STORAGE_BUCKET is defined.',
9191
)
92+
93+
94+
class S3BuildLanguagesStorage(S3PrivateBucketMixin, BuildMediaStorageMixin, S3Boto3Storage):
95+
96+
bucket_name = getattr(settings, 'S3_BUILD_LANGUAGES_STORAGE_BUCKET', None)
97+
98+
def __init__(self, *args, **kwargs):
99+
super().__init__(*args, **kwargs)
100+
101+
if not self.bucket_name:
102+
raise ImproperlyConfigured(
103+
'AWS S3 not configured correctly. '
104+
'Ensure S3_BUILD_LANGUAGES_STORAGE_BUCKET is defined.',
105+
)

0 commit comments

Comments
 (0)