Skip to content

Commit 4338b99

Browse files
committed
Merge branch 'master' of github.com:readthedocs/readthedocs.org into humitos/remove-web-related-tasks
2 parents 317428d + ecab363 commit 4338b99

File tree

16 files changed

+229
-64
lines changed

16 files changed

+229
-64
lines changed

dockerfiles/nginx/proxito.conf

+4-2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ server {
5656
add_header X-RTD-Path $rtd_path always;
5757
set $rtd_domain $upstream_http_x_rtd_domain;
5858
add_header X-RTD-Domain $rtd_domain always;
59-
set $rtd_method $upstream_http_x_rtd_version_method;
60-
add_header X-RTD-Version-Method $rtd_method always;
59+
set $rtd_version_method $upstream_http_x_rtd_version_method;
60+
add_header X-RTD-Version-Method $rtd_version_method always;
61+
set $rtd_project_method $upstream_http_x_rtd_project_method;
62+
add_header X-RTD-Project-Method $rtd_project_method always;
6163
set $rtd_redirect $upstream_http_x_rtd_redirect;
6264
add_header X-RTD-Redirect $rtd_redirect always;
6365
}

docs/custom_domains.rst

+5-13
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,14 @@ You can also host your documentation from your own domain.
4040
The SSL certificate issuance can take about one hour,
4141
you can see the status of the certificate on the domain page in your project.
4242

43-
For example, https://pip.pypa.io resolves, but is hosted on our infrastructure.
44-
As another example, fabric's dig record looks like this:
43+
As an example, fabric's dig record looks like this:
4544

4645
.. prompt:: bash $, auto
4746

48-
$ dig docs.fabfile.org
49-
...
50-
;; ANSWER SECTION:
51-
docs.fabfile.org. 7200 IN CNAME readthedocs.io.
52-
53-
.. note::
54-
55-
Some older setups configured a CNAME record pointing to ``readthedocs.org`` or another variation.
56-
While these continue to resolve,
57-
they do not yet allow us to acquire SSL certificates for those domains.
58-
Follow the new setup to have a SSL certificate.
47+
$ dig +short docs.fabfile.org
48+
readthedocs.io.
49+
104.17.33.82
50+
104.17.32.82
5951

6052
.. admonition:: Certificate Authority Authorization (CAA)
6153

docs/guides/autobuild-docs-for-pull-requests.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ you might hit one of these issues:
4242

4343
#. **Build status is not being reported on your Pull/Merge Request**.
4444
You need to make sure that you have granted access to the Read the Docs
45-
`OAuth App`_ to your/organizations GitHub account.
45+
`OAuth App`_ to your/organizations GitHub account. If you do not see "Read the Docs"
46+
among the `OAuth App`_, you might need to disconnect and reconnect to GitHub service.
4647
Also make sure your webhook is properly setup
4748
to handle events. You can setup or ``re-sync`` the webhook from your projects admin dashboard.
4849
Learn more about setting up webhooks from our :doc:`Webhook Documentation </webhooks>`.

docs/guides/feature-flags.rst

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ e.g. python-reno release notes manager is known to do that
3737

3838
``EXTERNAL_VERSION_BUILD``: :featureflags:`EXTERNAL_VERSION_BUILD`
3939

40+
``LIST_PACKAGES_INSTALLED_ENV``: :featureflags:`LIST_PACKAGES_INSTALLED_ENV`
41+
4042
``SHARE_SPHINX_DOCTREE``: :featureflags:`SHARE_SPHINX_DOCTREE`
4143

4244
By default, when Read the Docs runs Sphinx it passes a different output directory for the generated/parsed doctrees

readthedocs/conftest.py

-26
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,6 @@
11
import pytest
22
from rest_framework.test import APIClient
33

4-
5-
try:
6-
# TODO: this file is read/executed even when called from ``readthedocsinc``,
7-
# so it's overriding the options that we are defining in the ``conftest.py``
8-
# from the corporate site. We need to find a better way to avoid this.
9-
import readthedocsinc
10-
PYTEST_OPTIONS = ()
11-
except ImportError:
12-
PYTEST_OPTIONS = (
13-
# Options to set test environment
14-
('community', True),
15-
('corporate', False),
16-
('environment', 'readthedocs'),
17-
)
18-
19-
20-
def pytest_configure(config):
21-
for option, value in PYTEST_OPTIONS:
22-
setattr(config.option, option, value)
23-
24-
25-
@pytest.fixture(autouse=True)
26-
def settings_modification(settings):
27-
settings.CELERY_ALWAYS_EAGER = True
28-
29-
304
@pytest.fixture
315
def api_client():
326
return APIClient()

readthedocs/doc_builder/python_environments.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -425,9 +425,23 @@ def install_requirements_file(self, install):
425425
self.build_env.run(
426426
*args,
427427
cwd=self.checkout_path,
428-
bin_path=self.venv_bin() # noqa - no comma here in py27 :/
428+
bin_path=self.venv_bin(),
429429
)
430430

431+
def list_packages_installed(self):
432+
"""List packages installed in pip."""
433+
args = [
434+
self.venv_bin(filename='python'),
435+
'-m',
436+
'pip',
437+
'list',
438+
]
439+
self.build_env.run(
440+
*args,
441+
cwd=self.checkout_path,
442+
bin_path=self.venv_bin(),
443+
)
444+
431445

432446
class Conda(PythonEnvironment):
433447

@@ -627,3 +641,15 @@ def install_requirements_file(self, install):
627641
# as the conda environment was created by using the ``environment.yml``
628642
# defined by the user, there is nothing to update at this point
629643
pass
644+
645+
def list_packages_installed(self):
646+
"""List packages installed in conda."""
647+
args = [
648+
'conda',
649+
'list',
650+
]
651+
self.build_env.run(
652+
*args,
653+
cwd=self.checkout_path,
654+
bin_path=self.venv_bin(),
655+
)

readthedocs/projects/models.py

+13
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,8 @@ def add_features(sender, **kwargs):
15161516
CACHED_ENVIRONMENT = 'cached_environment'
15171517
CELERY_ROUTER = 'celery_router'
15181518
LIMIT_CONCURRENT_BUILDS = 'limit_concurrent_builds'
1519+
LIST_PACKAGES_INSTALLED_ENV = 'list_packages_installed_env'
1520+
VCS_REMOTE_LISTING = 'vcs_remote_listing'
15191521

15201522
FEATURES = (
15211523
(USE_SPHINX_LATEST, _('Use latest version of Sphinx')),
@@ -1594,6 +1596,17 @@ def add_features(sender, **kwargs):
15941596
LIMIT_CONCURRENT_BUILDS,
15951597
_('Limit the amount of concurrent builds'),
15961598
),
1599+
(
1600+
LIST_PACKAGES_INSTALLED_ENV,
1601+
_(
1602+
'List packages installed in the environment ("pip list" or "conda list") '
1603+
'on build\'s output',
1604+
),
1605+
),
1606+
(
1607+
VCS_REMOTE_LISTING,
1608+
_('Use remote listing in VCS (e.g. git ls-remote) if supported for sync versions'),
1609+
),
15971610
)
15981611

15991612
projects = models.ManyToManyField(

readthedocs/projects/tasks.py

+37-4
Original file line numberDiff line numberDiff line change
@@ -245,24 +245,37 @@ def sync_versions(self, version_repo):
245245
``sync_versions`` endpoint.
246246
"""
247247
version_post_data = {'repo': version_repo.repo_url}
248+
tags = None
249+
branches = None
250+
if all([
251+
version_repo.supports_lsremote,
252+
not version_repo.repo_exists(),
253+
self.project.has_feature(Feature.VCS_REMOTE_LISTING),
254+
]):
255+
# Do not use ``ls-remote`` if the VCS does not support it or if we
256+
# have already cloned the repository locally. The latter happens
257+
# when triggering a normal build.
258+
branches, tags = version_repo.lsremote
248259

249260
if all([
250261
version_repo.supports_tags,
251262
not self.project.has_feature(Feature.SKIP_SYNC_TAGS)
252263
]):
264+
tags = tags or version_repo.tags
253265
version_post_data['tags'] = [{
254266
'identifier': v.identifier,
255267
'verbose_name': v.verbose_name,
256-
} for v in version_repo.tags]
268+
} for v in tags]
257269

258270
if all([
259271
version_repo.supports_branches,
260272
not self.project.has_feature(Feature.SKIP_SYNC_BRANCHES)
261273
]):
274+
branches = branches or version_repo.branches
262275
version_post_data['branches'] = [{
263276
'identifier': v.identifier,
264277
'verbose_name': v.verbose_name,
265-
} for v in version_repo.branches]
278+
} for v in branches]
266279

267280
self.validate_duplicate_reserved_versions(version_post_data)
268281

@@ -372,7 +385,7 @@ def run(self, version_pk): # pylint: disable=arguments-differ
372385
# all the other cached things (Python packages, Sphinx,
373386
# virtualenv, etc)
374387
self.pull_cached_environment()
375-
self.sync_repo(environment)
388+
self.update_versions_from_repository(environment)
376389
return True
377390
except RepositoryError:
378391
# Do not log as ERROR handled exceptions
@@ -401,6 +414,24 @@ def run(self, version_pk): # pylint: disable=arguments-differ
401414
# Always return False for any exceptions
402415
return False
403416

417+
def update_versions_from_repository(self, environment):
418+
"""
419+
Update Read the Docs versions from VCS repository.
420+
421+
Depending if the VCS backend supports remote listing, we just list its branches/tags
422+
remotely or we do a full clone and local listing of branches/tags.
423+
"""
424+
version_repo = self.get_vcs_repo(environment)
425+
if any([
426+
not version_repo.supports_lsremote,
427+
not self.project.has_feature(Feature.VCS_REMOTE_LISTING),
428+
]):
429+
log.info('Syncing repository via full clone. project=%s', self.projec.slug)
430+
self.sync_repo(environment)
431+
else:
432+
log.info('Syncing repository via remote listing. project=%s', self.projec.slug)
433+
self.sync_versions(version_repo)
434+
404435

405436
@app.task(
406437
bind=True,
@@ -1116,6 +1147,8 @@ def setup_python_environment(self):
11161147
self.python_env.save_environment_json()
11171148
self.python_env.install_core_requirements()
11181149
self.python_env.install_requirements()
1150+
if self.project.has_feature(Feature.LIST_PACKAGES_INSTALLED_ENV):
1151+
self.python_env.list_packages_installed()
11191152

11201153
def build_docs(self):
11211154
"""
@@ -1170,7 +1203,7 @@ def build_docs_search(self):
11701203
For MkDocs search is indexed from its ``html`` artifacts.
11711204
And in sphinx is run using the rtd-sphinx-extension.
11721205
"""
1173-
return self.is_type_sphinx() and self.version.type != EXTERNAL
1206+
return self.is_type_sphinx()
11741207

11751208
def build_docs_localmedia(self):
11761209
"""Get local media files with separate build."""

readthedocs/proxito/middleware.py

+27-5
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,30 @@ def process_request(self, request): # noqa
132132
return None
133133

134134
def process_response(self, request, response): # noqa
135-
"""Set the Strict-Transport-Security (HSTS) header for a custom domain if max-age>0."""
136-
if hasattr(request, 'domain'):
135+
"""
136+
Set the Strict-Transport-Security (HSTS) header for docs sites.
137+
138+
* For the public domain, set the HSTS header if settings.PUBLIC_DOMAIN_USES_HTTPS
139+
* For custom domains, check the HSTS values on the Domain object.
140+
The domain object should be saved already in request.domain.
141+
"""
142+
host = request.get_host().lower().split(':')[0]
143+
public_domain = settings.PUBLIC_DOMAIN.lower().split(':')[0]
144+
145+
hsts_header_values = []
146+
147+
if not request.is_secure():
148+
# Only set the HSTS header if the request is over HTTPS
149+
return response
150+
151+
if settings.PUBLIC_DOMAIN_USES_HTTPS and public_domain in host:
152+
hsts_header_values = [
153+
'max-age=31536000',
154+
'includeSubDomains',
155+
'preload',
156+
]
157+
elif hasattr(request, 'domain'):
137158
domain = request.domain
138-
hsts_header_values = []
139159
if domain.hsts_max_age:
140160
hsts_header_values.append(f'max-age={domain.hsts_max_age}')
141161
# These other options don't make sense without max_age > 0
@@ -144,6 +164,8 @@ def process_response(self, request, response): # noqa
144164
if domain.hsts_preload:
145165
hsts_header_values.append('preload')
146166

147-
# See https://tools.ietf.org/html/rfc6797
148-
response['Strict-Transport-Security'] = '; '.join(hsts_header_values)
167+
if hsts_header_values:
168+
# See https://tools.ietf.org/html/rfc6797
169+
response['Strict-Transport-Security'] = '; '.join(hsts_header_values)
170+
149171
return response

readthedocs/proxito/tests/test_full.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,25 @@ def test_valid_project_as_invalid_subproject(self):
198198
resp = self.client.get(url, HTTP_HOST=host)
199199
self.assertEqual(resp.status_code, 404)
200200

201-
def test_response_hsts(self):
201+
def test_public_domain_hsts(self):
202+
host = 'project.dev.readthedocs.io'
203+
response = self.client.get('/', HTTP_HOST=host)
204+
self.assertFalse('strict-transport-security' in response)
205+
206+
response = self.client.get("/", HTTP_HOST=host, secure=True)
207+
self.assertFalse('strict-transport-security' in response)
208+
209+
with override_settings(PUBLIC_DOMAIN_USES_HTTPS=True):
210+
response = self.client.get('/', HTTP_HOST=host)
211+
self.assertFalse('strict-transport-security' in response)
212+
213+
response = self.client.get("/", HTTP_HOST=host, secure=True)
214+
self.assertEqual(
215+
response['strict-transport-security'],
216+
'max-age=31536000; includeSubDomains; preload',
217+
)
218+
219+
def test_custom_domain_response_hsts(self):
202220
hostname = 'docs.random.com'
203221
domain = fixture.get(
204222
Domain,
@@ -212,10 +230,16 @@ def test_response_hsts(self):
212230
response = self.client.get("/", HTTP_HOST=hostname)
213231
self.assertFalse('strict-transport-security' in response)
214232

233+
response = self.client.get("/", HTTP_HOST=hostname, secure=True)
234+
self.assertFalse('strict-transport-security' in response)
235+
215236
domain.hsts_max_age = 3600
216237
domain.save()
217238

218239
response = self.client.get("/", HTTP_HOST=hostname)
240+
self.assertFalse('strict-transport-security' in response)
241+
242+
response = self.client.get("/", HTTP_HOST=hostname, secure=True)
219243
self.assertTrue('strict-transport-security' in response)
220244
self.assertEqual(
221245
response['strict-transport-security'], 'max-age=3600',
@@ -225,7 +249,7 @@ def test_response_hsts(self):
225249
domain.hsts_preload = True
226250
domain.save()
227251

228-
response = self.client.get("/", HTTP_HOST=hostname)
252+
response = self.client.get("/", HTTP_HOST=hostname, secure=True)
229253
self.assertTrue('strict-transport-security' in response)
230254
self.assertEqual(
231255
response['strict-transport-security'], 'max-age=3600; includeSubDomains; preload',

readthedocs/proxito/views/mixins.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,15 @@ def _serve_docs_nginx(self, request, final_project, version_slug, path, download
129129
# Include the project & project-version so we can do larger purges if needed
130130
response['Cache-Tag'] = f'{final_project.slug}-{version_slug},{final_project.slug}'
131131
if hasattr(request, 'rtdheader'):
132-
response['X-RTD-Version-Method'] = 'rtdheader'
133-
if hasattr(request, 'subdomain'):
134-
response['X-RTD-Version-Method'] = 'subdomain'
132+
response['X-RTD-Project-Method'] = 'rtdheader'
133+
elif hasattr(request, 'subdomain'):
134+
response['X-RTD-Project-Method'] = 'subdomain'
135+
elif hasattr(request, 'cname'):
136+
response['X-RTD-Project-Method'] = 'cname'
135137
if hasattr(request, 'external_domain'):
136-
response['X-RTD-Version-Method'] = 'external_domain'
137-
if hasattr(request, 'cname'):
138-
response['X-RTD-Version-Method'] = 'cname'
138+
response['X-RTD-Version-Method'] = 'domain'
139+
else:
140+
response['X-RTD-Version-Method'] = 'path'
139141

140142
return response
141143

0 commit comments

Comments
 (0)