Skip to content
This repository was archived by the owner on Mar 18, 2022. It is now read-only.

Add the ability to add a docker config image #33

Merged
merged 14 commits into from
Dec 28, 2017
Merged
6 changes: 6 additions & 0 deletions docs/spec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ Following mapping keys are supported (all but the marked once are optional):
- ``pdf``, default: ``false``
- ``epub``, default: ``false``

``build``
Options for setting the docker configuration.

``image``
This sets the build image to use on the build, as defined `here <https://github.com/rtfd/readthedocs-docker-images/blob/master/CONTRIBUTING.rst#releases>`_.

``python``
Python specific configuration. All builds are executed inside a
virtualenv. This config can customize the virtualenv before running the
Expand Down
75 changes: 68 additions & 7 deletions readthedocs_build/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@
from .find import find_one
from .parser import ParseError
from .parser import parse
from .validation import validate_bool
from .validation import validate_choice
from .validation import validate_directory
from .validation import validate_file
from .validation import validate_string
from .validation import ValidationError
from .validation import (validate_bool, validate_choice, validate_directory,
validate_file, validate_string, ValidationError)


__all__ = (
Expand All @@ -30,6 +26,23 @@
TYPE_REQUIRED = 'type-required'
PYTHON_INVALID = 'python-invalid'

DOCKER_DEFAULT_IMAGE = 'readthedocs/build'
DOCKER_DEFAULT_VERSION = '2.0'
# These map to coordisponding settings in the .org,
# so they haven't been renamed.
DOCKER_IMAGE = '{}:{}'.format(DOCKER_DEFAULT_IMAGE, DOCKER_DEFAULT_VERSION)
DOCKER_IMAGE_SETTINGS = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shoulnd't we have the same .update() we have in .org?

https://github.com/rtfd/readthedocs.org/pull/3339/files#diff-3fbfb50cb375355069e83003f500d2c8R44

or at least, the getattr since otherwise there is no way to override this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, fixed that below -- I don't want this code to depend on RTD settings, so we pass it in.

'readthedocs/build:1.0': {
'python': {'supported_versions': [2, 2.7, 3, 3.4]},
},
'readthedocs/build:2.0': {
'python': {'supported_versions': [2, 2.7, 3, 3.5]},
},
'readthedocs/build:latest': {
'python': {'supported_versions': [2, 2.7, 3, 3.3, 3.4, 3.5, 3.6]},
},
}


class ConfigError(Exception):

Expand Down Expand Up @@ -79,6 +92,7 @@ class BuildConfig(dict):
'"python.extra_requirements" section must be a list.')

PYTHON_SUPPORTED_VERSIONS = [2, 2.7, 3, 3.3, 3.4, 3.5, 3.6]
DOCKER_SUPPORTED_VERSIONS = ['1.0', '2.0', 'latest']

def __init__(self, env_config, raw_config, source_file, source_position):
self.env_config = env_config
Expand Down Expand Up @@ -145,6 +159,9 @@ def validate(self):
# Validate env_config.
self.validate_output_base()

# Validate the build environment first
self.validate_build() # Must happen before `validate_python`!

# Validate raw_config. Order matters.
self.validate_name()
self.validate_type()
Expand Down Expand Up @@ -205,6 +222,50 @@ def validate_base(self):
base = validate_directory(base, base_path)
self['base'] = base

def validate_build(self):
"""
Validate the build config settings.

This is a bit complex,
so here is the logic:

* We take the default image & version if it's specific in the environment
* Then update the _version_ from the users config
* Then append the default _image_, since users can't change this
* Then update the env_config with the settings for that specific image
- This is currently used for a build image -> python version mapping

This means we can use custom docker _images_,
but can't change the supported _versions_ that users have defined.
"""
# Defaults
if 'build' in self.env_config:
build = self.env_config['build']
else:
build = {'image': DOCKER_IMAGE}

# User specified
if 'build' in self.raw_config:
_build = self.raw_config['build']
if 'image' in _build:
with self.catch_validation_error('build'):
build['image'] = validate_choice(
str(_build['image']),
self.DOCKER_SUPPORTED_VERSIONS,
)
if ':' not in build['image']:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems that this condition will be always True, since we are accepting 1.0, 2.0 and latest as valid choices (from DOCKER_SUPPORTED_VERSIONS)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the build image is passed from RTD it will be fully qualified already.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the default (not from YAML)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I follow you. If the image is passed from RTD or not from YAML we won't be inside if 'build' in self.raw_config since I understood that raw_config is the YAML. So, maybe this if should be outside this block.

# Prepend proper image name to user's image name
build['image'] = '{}:{}'.format(
DOCKER_DEFAULT_IMAGE,
build['image']
)
if build['image'] in DOCKER_IMAGE_SETTINGS:
# Update docker settings from image name
self.env_config.update(
DOCKER_IMAGE_SETTINGS[build['image']]
)
self['build'] = build

def validate_python(self):
python = {
'use_system_site_packages': False,
Expand Down Expand Up @@ -280,7 +341,7 @@ def validate_python(self):
pass
python['version'] = validate_choice(
version,
self.get_valid_python_versions()
self.get_valid_python_versions(),
)

self['python'] = python
Expand Down
64 changes: 64 additions & 0 deletions readthedocs_build/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,70 @@ def it_fails_if_base_does_not_exist(tmpdir):
assert excinfo.value.code == INVALID_PATH


def describe_validate_build():

def it_fails_if_build_is_invalid_option(tmpdir):
apply_fs(tmpdir, minimal_config)
build = BuildConfig(
{},
{'build': {'image': 3.0}},
source_file=str(tmpdir.join('readthedocs.yml')),
source_position=0)
with raises(InvalidConfig) as excinfo:
build.validate_build()
assert excinfo.value.key == 'build'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why we don't use something like regular unittest.TestCase and assertEqual and those stuffs in this project?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is just how this code was written originally. shrug

assert excinfo.value.code == INVALID_CHOICE

def it_fails_on_python_validation(tmpdir):
apply_fs(tmpdir, minimal_config)
build = BuildConfig(
{},
{
'build': {'image': 1.0},
'python': {'version': '3.3'},
},
source_file=str(tmpdir.join('readthedocs.yml')),
source_position=0)
build.validate_build()
with raises(InvalidConfig) as excinfo:
build.validate_python()
assert excinfo.value.key == 'python.version'
assert excinfo.value.code == INVALID_CHOICE

def it_works_on_python_validation(tmpdir):
apply_fs(tmpdir, minimal_config)
build = BuildConfig(
{},
{
'build': {'image': 'latest'},
'python': {'version': '3.3'},
},
source_file=str(tmpdir.join('readthedocs.yml')),
source_position=0)
build.validate_build()
build.validate_python()

def it_works(tmpdir):
apply_fs(tmpdir, minimal_config)
build = BuildConfig(
{},
{'build': {'image': 'latest'}},
source_file=str(tmpdir.join('readthedocs.yml')),
source_position=0)
build.validate_build()
assert build['build']['image'] == 'readthedocs/build:latest'

def default(tmpdir):
apply_fs(tmpdir, minimal_config)
build = BuildConfig(
{},
{},
source_file=str(tmpdir.join('readthedocs.yml')),
source_position=0)
build.validate_build()
assert build['build']['image'] == 'readthedocs/build:2.0'


def test_build_validate_calls_all_subvalidators(tmpdir):
apply_fs(tmpdir, minimal_config)
build = BuildConfig(
Expand Down
4 changes: 2 additions & 2 deletions requirements/tests.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mock==1.3.0
pytest>=2.7
pytest-describe>=0.10.1
pytest==3.2.5
pytest-describe==0.11.0
pytest-xdist>=1.12
tox>=2.1