Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build: use new Docker images from design document #8453

Merged
merged 42 commits into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c3cec8a
Build: use new Docker images from design document
humitos Aug 31, 2021
b553dc8
Script to compile a version and upload it to the cache (S3)
humitos Sep 2, 2021
3c3e51e
Compile Python with `--enable-shared`
humitos Sep 2, 2021
49e489d
Terminate the script on failures
humitos Sep 2, 2021
8948587
Pass --env to docker exec properly
humitos Sep 2, 2021
677ec6c
Typo
humitos Sep 2, 2021
d2da08d
Comment the Python compilation options for now
humitos Sep 2, 2021
0415235
Rename variable to not hide `os` module
humitos Sep 2, 2021
94ce225
Make compile&upload to work with Python 2.7 and its dependencies
humitos Sep 2, 2021
fd322bb
Don't run tar with `--verbose`
humitos Sep 2, 2021
1ea6c60
Use bash for the script
humitos Sep 2, 2021
d86e1b9
Add `awscli` dependency to upload .tar.gz to S3 (MinIO)
humitos Sep 2, 2021
6d2f4ed
Reduce sleep time
humitos Sep 2, 2021
5f0df77
Pass the variables to the sub-process so `aws` can access to them
humitos Sep 2, 2021
1029806
Fix MinIO default password in script
humitos Sep 2, 2021
3bac624
Pass the variables to `aws` again :)
humitos Sep 2, 2021
81d957b
Merge branch 'master' into humitos/build-new-docker-image
humitos Sep 2, 2021
eb932b8
Update `if` to avoid installing Python dependencies on conda/mamba
humitos Sep 2, 2021
128a361
Merge branch 'humitos/build-new-docker-image' of github.com:readthedo…
humitos Sep 2, 2021
009bff1
Remove the container used on timeout
humitos Sep 2, 2021
213a4bd
Rename `install_languages` to `install_build_languages`
humitos Sep 8, 2021
f34b3cb
Rename `build.languages` to `build.tools`
humitos Sep 20, 2021
2d9f899
Merge branch 'master' of github.com:readthedocs/readthedocs.org into …
humitos Sep 20, 2021
7964292
Decide whether to use conda/mamba
humitos Sep 20, 2021
c2a4b2f
Minor fixes
humitos Sep 20, 2021
00c2ec1
Use `ubuntu-20.04` as `build.os`
humitos Sep 20, 2021
46ca944
Select proper docker image based on build.os
humitos Sep 20, 2021
6b7fa2f
Move `install_build_tools()` inside `setup_python_environment()`
humitos Sep 20, 2021
edc52c7
Decide conda or mamba executable based on config.build.tools.python
humitos Sep 20, 2021
80bba7f
Install tools only if build.os and build.tools are defined
humitos Sep 20, 2021
e6c05a4
Remove feature flag
humitos Sep 20, 2021
0e0c08b
Better syntax
humitos Sep 20, 2021
c5f920e
Update with latest config object changes
humitos Sep 21, 2021
5f8b4e8
Merge branch 'master' of github.com:readthedocs/readthedocs.org into …
humitos Sep 21, 2021
2131b74
Test we are executing the asdf commands required
humitos Sep 21, 2021
f2735ae
Feedback addressed with new config file options
humitos Sep 21, 2021
063d2b3
Lint
humitos Sep 21, 2021
1e42d8d
Merge branch 'master' of github.com:readthedocs/readthedocs.org into …
humitos Sep 21, 2021
326b231
Fix tests
humitos Sep 22, 2021
fba017d
Patch pyenv-build to not upgrade conda
humitos Sep 23, 2021
9528849
Do not patch pyenv-build anymore
humitos Sep 27, 2021
94231b9
Explain $1 and $2 arguments for the script
humitos Sep 27, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions readthedocs/doc_builder/environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,8 +782,8 @@ def __init__(self, *args, **kwargs):
if self.project.has_feature(Feature.USE_TESTING_BUILD_IMAGE):
self.container_image = 'readthedocs/build:testing'
# the image set by user or,
if self.config and self.config.build.image:
self.container_image = self.config.build.image
if self.config and self.config.docker_image:
self.container_image = self.config.docker_image
# the image overridden by the project (manually set by an admin).
if self.project.container_image:
self.container_image = self.project.container_image
Expand Down
126 changes: 122 additions & 4 deletions readthedocs/doc_builder/python_environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import logging
import os
import shutil
import tarfile

import yaml
from django.conf import settings
Expand All @@ -22,6 +23,7 @@
from readthedocs.doc_builder.loader import get_builder_class
from readthedocs.projects.constants import LOG_TEMPLATE
from readthedocs.projects.models import Feature
from readthedocs.storage import build_tools_storage

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -72,6 +74,112 @@ def delete_existing_venv_dir(self):
)
shutil.rmtree(venv_dir)

def install_build_tools(self):
if settings.RTD_DOCKER_COMPOSE:
# Create a symlink for ``root`` user to use the same ``.asdf``
# installation than ``docs`` user. Required for local building
# since everything is run as ``root`` when using Local Development
# instance
cmd = [
'ln',
'-s',
os.path.join(settings.RTD_DOCKER_WORKDIR, '.asdf'),
'/root/.asdf',
]
self.build_env.run(
*cmd,
)

for tool, version in self.config.build.tools.items():
version = version.full_version # e.g. 3.9 -> 3.9.7

# TODO: generate the correct path for the Python version
# tool_path = f'{self.config.build.os}/{tool}/2021-08-30/{version}.tar.gz'
tool_path = f'{self.config.build.os}-{tool}-{version}.tar.gz'
tool_version_cached = build_tools_storage.exists(tool_path)
if tool_version_cached:
remote_fd = build_tools_storage.open(tool_path, mode='rb')
with tarfile.open(fileobj=remote_fd) as tar:
# Extract it on the shared path between host and Docker container
extract_path = os.path.join(self.project.doc_path, 'tools')
tar.extractall(extract_path)

# Move the extracted content to the ``asdf`` installation
cmd = [
'mv',
f'{extract_path}/{version}',
os.path.join(
settings.RTD_DOCKER_WORKDIR,
f'.asdf/installs/{tool}/{version}',
),
]
self.build_env.run(
*cmd,
)
else:
log.debug(
'Cached version for tool not found. os=%s tool=%s version=% filename=%s',
self.config.build.os,
tool,
version,
tool_path,
)
# If the tool version selected is not available from the
# cache we compile it at build time
cmd = [
# TODO: make this environment variable to work
# 'PYTHON_CONFIGURE_OPTS="--enable-shared"',
'asdf',
'install',
tool,
version,
]
self.build_env.run(
*cmd,
)

# Make the tool version chosen by the user the default one
cmd = [
'asdf',
'global',
tool,
version,
]
self.build_env.run(
*cmd,
)

# Recreate shims for this tool to make the new version
# installed available
cmd = [
'asdf',
'reshim',
tool,
]
self.build_env.run(
*cmd,
)

if all([
tool == 'python',
# Do not install them if the tool version was cached
not tool_version_cached,
# Do not install them on conda/mamba
self.config.python_interpreter == 'python',
]):
# Install our own requirements if the version is compiled
cmd = [
'python',
'-mpip',
'install',
'-U',
'virtualenv',
'setuptools',
]
self.build_env.run(
*cmd,
)

def install_requirements(self):
"""Install all requirements from the config object."""
for install in self.config.python.install:
Expand Down Expand Up @@ -214,7 +322,7 @@ def is_obsolete(self):
env_build_hash = env_build.get('hash', None)

if isinstance(self.build_env, DockerBuildEnvironment):
build_image = self.config.build.image or DOCKER_IMAGE
build_image = self.config.docker_image
image_hash = self.build_env.image_hash
else:
# e.g. LocalBuildEnvironment
Expand Down Expand Up @@ -269,7 +377,7 @@ def save_environment_json(self):
}

if isinstance(self.build_env, DockerBuildEnvironment):
build_image = self.config.build.image or DOCKER_IMAGE
build_image = self.config.docker_image
data.update({
'build': {
'image': build_image,
Expand Down Expand Up @@ -317,6 +425,7 @@ def setup_base(self):
cli_args.append(
self.venv_path(),
)

self.build_env.run(
self.config.python_interpreter,
*cli_args,
Expand Down Expand Up @@ -496,6 +605,11 @@ def conda_bin_name(self):
See https://github.com/QuantStack/mamba
"""
# Config file using ``build.tools.python``
if self.config.using_build_tools:
return self.config.python_interpreter

# Config file using ``conda``
if self.project.has_feature(Feature.CONDA_USES_MAMBA):
return 'mamba'
return 'conda'
Expand Down Expand Up @@ -556,8 +670,12 @@ def setup_base(self):
self._append_core_requirements()
self._show_environment_yaml()

# TODO: remove it when ``mamba`` is installed in the Docker image
if self.project.has_feature(Feature.CONDA_USES_MAMBA):
if all([
# The project has CONDA_USES_MAMBA feature enabled and,
self.project.has_feature(Feature.CONDA_USES_MAMBA),
# the project is not using ``build.tools``
not self.config.using_build_tools,
]):
self._install_mamba()

self.build_env.run(
Expand Down
9 changes: 8 additions & 1 deletion readthedocs/projects/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,10 @@ def run_build(self, record):
# Environment used for building code, usually with Docker
with self.build_env:
python_env_cls = Virtualenv
if self.config.conda is not None:
if any([
self.config.conda is not None,
self.config.python_interpreter in ('conda', 'mamba'),
]):
log.info(
LOG_TEMPLATE,
{
Expand Down Expand Up @@ -1164,6 +1167,10 @@ def setup_python_environment(self):
else:
self.python_env.delete_existing_build_dir()

# Install all ``build.tools`` specified by the user
if self.config.using_build_tools:
self.python_env.install_build_tools()

self.python_env.setup_base()
self.python_env.save_environment_json()
self.python_env.install_core_requirements()
Expand Down
154 changes: 154 additions & 0 deletions readthedocs/rtd_tests/tests/test_celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from unittest.mock import MagicMock, patch

from allauth.socialaccount.models import SocialAccount
from django.conf import settings
from django.contrib.auth.models import User
from django.test import TestCase
from django_dynamic_fixture import get
Expand Down Expand Up @@ -541,3 +542,156 @@ def test_install_apt_packages(self, load_config, run):
user='root:root',
)
)

@patch('readthedocs.projects.tasks.UpdateDocsTaskStep.build_docs', new=MagicMock)
@patch('readthedocs.projects.tasks.UpdateDocsTaskStep.setup_vcs', new=MagicMock)
@patch.object(BuildEnvironment, 'run')
@patch('readthedocs.doc_builder.config.load_config')
def test_build_tools(self, load_config, build_run):
config = BuildConfigV2(
{},
{
'version': 2,
'build': {
'os': 'ubuntu-20.04',
'tools': {
'python': '3.10',
'nodejs': '16',
'rust': '1.55',
'golang': '1.17',
},
},
},
source_file='readthedocs.yml',
)
config.validate()
load_config.return_value = config

version = self.project.versions.first()
build = get(
Build,
project=self.project,
version=version,
)
with mock_api(self.repo):
result = tasks.update_docs_task.delay(
version.pk,
build_pk=build.pk,
record=False,
intersphinx=False,
)
self.assertTrue(result.successful())
self.assertEqual(build_run.call_count, 14)

python_version = settings.RTD_DOCKER_BUILD_SETTINGS['tools']['python']['3.10']
nodejs_version = settings.RTD_DOCKER_BUILD_SETTINGS['tools']['nodejs']['16']
rust_version = settings.RTD_DOCKER_BUILD_SETTINGS['tools']['rust']['1.55']
golang_version = settings.RTD_DOCKER_BUILD_SETTINGS['tools']['golang']['1.17']
self.assertEqual(
build_run.call_args_list,
[
mock.call('asdf', 'install', 'python', python_version),
mock.call('asdf', 'global', 'python', python_version),
mock.call('asdf', 'reshim', 'python'),
mock.call('python', '-mpip', 'install', '-U', 'virtualenv', 'setuptools'),
mock.call('asdf', 'install', 'nodejs', nodejs_version),
mock.call('asdf', 'global', 'nodejs', nodejs_version),
mock.call('asdf', 'reshim', 'nodejs'),
mock.call('asdf', 'install', 'rust', rust_version),
mock.call('asdf', 'global', 'rust', rust_version),
mock.call('asdf', 'reshim', 'rust'),
mock.call('asdf', 'install', 'golang', golang_version),
mock.call('asdf', 'global', 'golang', golang_version),
mock.call('asdf', 'reshim', 'golang'),
mock.ANY,
],
)

@patch('readthedocs.projects.tasks.UpdateDocsTaskStep.build_docs', new=MagicMock)
@patch('readthedocs.projects.tasks.UpdateDocsTaskStep.setup_vcs', new=MagicMock)
@patch('readthedocs.doc_builder.python_environments.tarfile')
@patch('readthedocs.doc_builder.python_environments.build_tools_storage')
@patch.object(BuildEnvironment, 'run')
@patch('readthedocs.doc_builder.config.load_config')
def test_build_tools_cached(self, load_config, build_run, build_tools_storage, tarfile):
config = BuildConfigV2(
{},
{
'version': 2,
'build': {
'os': 'ubuntu-20.04',
'tools': {
'python': '3.10',
'nodejs': '16',
'rust': '1.55',
'golang': '1.17',
},
},
},
source_file='readthedocs.yml',
)
config.validate()
load_config.return_value = config

build_tools_storage.open.return_value = b''
build_tools_storage.exists.return_value = True
tarfile.open.return_value.__enter__.return_value.extract_all.return_value = None

version = self.project.versions.first()
build = get(
Build,
project=self.project,
version=version,
)
with mock_api(self.repo):
result = tasks.update_docs_task.delay(
version.pk,
build_pk=build.pk,
record=False,
intersphinx=False,
)
self.assertTrue(result.successful())
self.assertEqual(build_run.call_count, 13)

python_version = settings.RTD_DOCKER_BUILD_SETTINGS['tools']['python']['3.10']
nodejs_version = settings.RTD_DOCKER_BUILD_SETTINGS['tools']['nodejs']['16']
rust_version = settings.RTD_DOCKER_BUILD_SETTINGS['tools']['rust']['1.55']
golang_version = settings.RTD_DOCKER_BUILD_SETTINGS['tools']['golang']['1.17']
self.assertEqual(
# NOTE: casting the first argument as `list()` shows a better diff
# explaining where the problem is
list(build_run.call_args_list),
[
mock.call(
'mv',
# Use mock.ANY here because path differs when ran locally
# and on CircleCI
mock.ANY,
f'/home/docs/.asdf/installs/python/{python_version}',
),
mock.call('asdf', 'global', 'python', python_version),
mock.call('asdf', 'reshim', 'python'),
mock.call(
'mv',
mock.ANY,
f'/home/docs/.asdf/installs/nodejs/{nodejs_version}',
),
mock.call('asdf', 'global', 'nodejs', nodejs_version),
mock.call('asdf', 'reshim', 'nodejs'),
mock.call(
'mv',
mock.ANY,
f'/home/docs/.asdf/installs/rust/{rust_version}',
),
mock.call('asdf', 'global', 'rust', rust_version),
mock.call('asdf', 'reshim', 'rust'),
mock.call(
'mv',
mock.ANY,
f'/home/docs/.asdf/installs/golang/{golang_version}',
),
mock.call('asdf', 'global', 'golang', golang_version),
mock.call('asdf', 'reshim', 'golang'),
mock.ANY,
],
)
1 change: 1 addition & 0 deletions readthedocs/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ def USE_PROMOS(self): # noqa
# https://docs.readthedocs.io/page/development/settings.html#rtd-build-media-storage
RTD_BUILD_MEDIA_STORAGE = 'readthedocs.builds.storage.BuildMediaFileSystemStorage'
RTD_BUILD_ENVIRONMENT_STORAGE = 'readthedocs.builds.storage.BuildMediaFileSystemStorage'
RTD_BUILD_TOOLS_STORAGE = 'readthedocs.builds.storage.BuildMediaFileSystemStorage'
RTD_BUILD_COMMANDS_STORAGE = 'readthedocs.builds.storage.BuildMediaFileSystemStorage'

@property
Expand Down
Loading