Skip to content

Build: use new Docker images from design document #8453

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

Merged
merged 42 commits into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from 20 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
3 changes: 3 additions & 0 deletions readthedocs/doc_builder/environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,9 @@ def __init__(self, *args, **kwargs):
# the image set by user or,
if self.config and self.config.build.image:
self.container_image = self.config.build.image
# the new Docker image structure or,
if self.project.has_feature(Feature.USE_NEW_DOCKER_IMAGES_STRUCTURE):
self.container_image = 'readthedocs/build:ubuntu20'
# 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
108 changes: 107 additions & 1 deletion 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_languages_storage

log = logging.getLogger(__name__)

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

def install_languages(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',
'/home/docs/.asdf',
'/root/.asdf',
]
self.build_env.run(
*cmd,
)

# TODO: do not use a Feature flag here, but check for ``build.os`` and
# ``build.languages`` instead
if self.project.has_feature(Feature.USE_NEW_DOCKER_IMAGES_STRUCTURE):
# TODO: iterate over ``build.languages`` and install all languages
# specified for this project
build_os = 'ubuntu20'
language = 'python'
version = '3.9.6'

# TODO: generate the correct path for the Python version
# language_path = f'{build_os}/{language}/2021-08-30/{version}.tar.gz'
language_path = f'{build_os}-{language}-{version}.tar.gz'
language_version_cached = build_languages_storage.exists(language_path)
if language_version_cached:
remote_fd = build_languages_storage.open(language_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, 'languages')
tar.extractall(extract_path)

# Move the extracted content to the ``asdf`` installation
cmd = [
'mv',
f'{extract_path}/{version}',
f'/home/docs/.asdf/installs/{language}/{version}',
]
self.build_env.run(
*cmd,
)
else:
# If the language 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',
language,
version,
]
self.build_env.run(
*cmd,
)

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

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

if language == 'python' and not language_version_cached:
# Install our own requirements if the version is compiled
cmd = [
'python',
'-m'
'pip',
'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 @@ -317,8 +416,15 @@ def setup_base(self):
cli_args.append(
self.venv_path(),
)

# TODO: make ``self.config.python_interpreter`` return the correct value
if self.project.has_feature(Feature.USE_NEW_DOCKER_IMAGES_STRUCTURE):
python_interpreter = 'python'
else:
python_interpreter = self.config.python_interpreter

self.build_env.run(
self.config.python_interpreter,
python_interpreter,
*cli_args,
# Don't use virtualenv bin that doesn't exist yet
bin_path=None,
Expand Down
5 changes: 5 additions & 0 deletions readthedocs/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1669,6 +1669,7 @@ def add_features(sender, **kwargs):
USE_SPHINX_BUILDERS = 'use_sphinx_builders'
DEDUPLICATE_BUILDS = 'deduplicate_builds'
DONT_CREATE_INDEX = 'dont_create_index'
USE_NEW_DOCKER_IMAGES_STRUCTURE = 'use_new_docker_images_structure'

FEATURES = (
(ALLOW_DEPRECATED_WEBHOOKS, _('Allow deprecated webhook views')),
Expand All @@ -1690,6 +1691,10 @@ def add_features(sender, **kwargs):
USE_TESTING_BUILD_IMAGE,
_('Use Docker image labelled as `testing` to build the docs'),
),
(
USE_NEW_DOCKER_IMAGES_STRUCTURE,
_('Use new Docker images that install languages at build time'),
),
(
API_LARGE_DATA,
_('Try alternative method of posting large data'),
Expand Down
1 change: 1 addition & 0 deletions readthedocs/projects/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,7 @@ def update_app_instances(

def setup_build(self):
self.install_system_dependencies()
self.python_env.install_languages()
self.setup_python_environment()

def setup_python_environment(self):
Expand Down
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_LANGUAGES_STORAGE = 'readthedocs.builds.storage.BuildMediaFileSystemStorage'
RTD_BUILD_COMMANDS_STORAGE = 'readthedocs.builds.storage.BuildMediaFileSystemStorage'

@property
Expand Down
3 changes: 3 additions & 0 deletions readthedocs/settings/docker_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ def show_debug_toolbar(request):
RTD_BUILD_MEDIA_STORAGE = 'readthedocs.storage.s3_storage.S3BuildMediaStorage'
# Storage backend for build cached environments
RTD_BUILD_ENVIRONMENT_STORAGE = 'readthedocs.storage.s3_storage.S3BuildEnvironmentStorage'
# Storage backend for build languages
RTD_BUILD_LANGUAGES_STORAGE = 'readthedocs.storage.s3_storage.S3BuildLanguagesStorage'
# Storage for static files (those collected with `collectstatic`)
STATICFILES_STORAGE = 'readthedocs.storage.s3_storage.S3StaticStorage'

Expand All @@ -142,6 +144,7 @@ def show_debug_toolbar(request):
S3_MEDIA_STORAGE_BUCKET = 'media'
S3_BUILD_COMMANDS_STORAGE_BUCKET = 'builds'
S3_BUILD_ENVIRONMENT_STORAGE_BUCKET = 'envs'
S3_BUILD_LANGUAGES_STORAGE_BUCKET = 'languages'
S3_STATIC_STORAGE_BUCKET = 'static'
S3_STATIC_STORAGE_OVERRIDE_HOSTNAME = 'community.dev.readthedocs.io'
S3_MEDIA_STORAGE_OVERRIDE_HOSTNAME = 'community.dev.readthedocs.io'
Expand Down
6 changes: 6 additions & 0 deletions readthedocs/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ def _setup(self):
self._wrapped = get_storage_class(settings.RTD_BUILD_COMMANDS_STORAGE)()


class ConfiguredBuildLanguagesStorage(LazyObject):
def _setup(self):
self._wrapped = get_storage_class(settings.RTD_BUILD_LANGUAGES_STORAGE)()


build_media_storage = ConfiguredBuildMediaStorage()
build_environment_storage = ConfiguredBuildEnvironmentStorage()
build_commands_storage = ConfiguredBuildCommandsStorage()
build_languages_storage = ConfiguredBuildLanguagesStorage()
14 changes: 14 additions & 0 deletions readthedocs/storage/s3_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,17 @@ def __init__(self, *args, **kwargs):
'AWS S3 not configured correctly. '
'Ensure S3_BUILD_ENVIRONMENT_STORAGE_BUCKET is defined.',
)


class S3BuildLanguagesStorage(S3PrivateBucketMixin, BuildMediaStorageMixin, S3Boto3Storage):

bucket_name = getattr(settings, 'S3_BUILD_LANGUAGES_STORAGE_BUCKET', None)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

if not self.bucket_name:
raise ImproperlyConfigured(
'AWS S3 not configured correctly. '
'Ensure S3_BUILD_LANGUAGES_STORAGE_BUCKET is defined.',
)
3 changes: 3 additions & 0 deletions requirements/docker.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ argh==0.26.2

# run tests
tox==3.24.3

# AWS utilities to use against MinIO
awscli==1.20.34
97 changes: 97 additions & 0 deletions scripts/compile_version_upload_s3.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/bin/bash
#
#
# Script to compile a languge version and upload it to the cache.
#
# This script automates the process to build and upload a Python/Node/Rust/Go
# version and upload it to S3 making it available for the builders. When a
# pre-compiled version is available in the cache, builds are faster because they
# don't have to donwload and compile.
#
# LOCAL DEVELOPMENT ENVIRONMENT
# https://docs.readthedocs.io/en/latest/development/install.html
#
# You can run this script from you local environment to create cached version
# and upload them to MinIO. For this, it's required that you have the MinIO
# instance running before executing this script command:
#
# inv docker.up
#
#
# PRODUCTION ENVIRONMENT
#
# To create a pre-compiled cached version and make it available on production,
# the script has to be ran from a builder (build-default or build-large) and
# it's required to set the following environment variables for an IAM user with
# permissions on ``language`` S3's bucket:
#
# AWS_ACCESS_KEY_ID
# AWS_SECRET_ACCESS_KEY
# AWS_ENDPOINT_URL
#
#
# USAGE
#
# ./scripts/compile_version_upload.sh python 3.9.6
#
#

set -e

# Define variables
SLEEP=350
OS="ubuntu20"
LANGUAGE=$1
VERSION=$2

# Spin up a container with the Ubuntu 20.04 LTS image
CONTAINER_ID=$(docker run --user docs --rm --detach readthedocs/build:$OS sleep $SLEEP)
echo "Running all the commands in Docker container: $CONTAINER_ID"

# Install the language version requested
if [[ $LANGUAGE == "python" ]]
then
docker exec --env PYTHON_CONFIGURE_OPTS="--enable-shared" $CONTAINER_ID asdf install $LANGUAGE $VERSION
else
docker exec $CONTAINER_ID asdf install $LANGUAGE $VERSION
fi

# Set the default version and reshim
docker exec $CONTAINER_ID asdf global $LANGUAGE $VERSION
docker exec $CONTAINER_ID asdf reshim $LANGUAGE

# Install dependencies for this version
if [[ $LANGUAGE == "python" ]] && [[ ! $VERSION =~ (^miniconda.*|^mambaforge.*) ]]
then
RTD_PIP_VERSION=21.2.4
RTD_SETUPTOOLS_VERSION=57.4.0
RTD_VIRTUALENV_VERSION=20.7.2

if [[ $VERSION == "2.7.18" ]]
then
# Pin to the latest versions supported on Python 2.7
RTD_PIP_VERSION=20.3.4
RTD_SETUPTOOLS_VERSION=44.1.1
RTD_VIRTUALENV_VERSION=20.7.2
fi
docker exec $CONTAINER_ID $LANGUAGE -m pip install -U pip==$RTD_PIP_VERSION setuptools==$RTD_SETUPTOOLS_VERSION virtualenv==$RTD_VIRTUALENV_VERSION
fi

# Compress it as a .tar.gz without include the full path in the compressed file
docker exec $CONTAINER_ID tar --create --gzip --directory=/home/docs/.asdf/installs/$LANGUAGE --file=$OS-$LANGUAGE-$VERSION.tar.gz $VERSION

# Copy the .tar.gz from the container to the host
docker cp $CONTAINER_ID:/home/docs/$OS-$LANGUAGE-$VERSION.tar.gz .

# Kill the container
docker container kill $CONTAINER_ID

# Upload the .tar.gz to S3
AWS_ENDPOINT_URL="${AWS_ENDPOINT_URL:-http://localhost:9000}"
AWS_LANGUAGES_BUCKET="${AWS_LANGUAGES_BUCKET:-languages}"
AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-admin}" \
AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-password}" \
aws --endpoint-url $AWS_ENDPOINT_URL s3 cp $OS-$LANGUAGE-$VERSION.tar.gz s3://$AWS_LANGUAGES_BUCKET

# Delete the .tar.gz file from the host
rm $OS-$LANGUAGE-$VERSION.tar.gz