From 6595dd47c0463ce630161649a21e604f81596519 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Fri, 14 Jun 2019 23:59:59 +0200 Subject: [PATCH 1/9] Complete process --- .../assets/{nginx_cert.pem => cert.pem} | 0 .../assets/{nginx_key.pem => key.pem} | 0 .../certbot_integration_tests/conftest.py | 2 +- .../utils/acme_server.py | 213 +++++++++--------- .../utils/pebble_artifacts.py | 52 +++++ 5 files changed, 164 insertions(+), 103 deletions(-) rename certbot-ci/certbot_integration_tests/assets/{nginx_cert.pem => cert.pem} (100%) rename certbot-ci/certbot_integration_tests/assets/{nginx_key.pem => key.pem} (100%) create mode 100644 certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py diff --git a/certbot-ci/certbot_integration_tests/assets/nginx_cert.pem b/certbot-ci/certbot_integration_tests/assets/cert.pem similarity index 100% rename from certbot-ci/certbot_integration_tests/assets/nginx_cert.pem rename to certbot-ci/certbot_integration_tests/assets/cert.pem diff --git a/certbot-ci/certbot_integration_tests/assets/nginx_key.pem b/certbot-ci/certbot_integration_tests/assets/key.pem similarity index 100% rename from certbot-ci/certbot_integration_tests/assets/nginx_key.pem rename to certbot-ci/certbot_integration_tests/assets/key.pem diff --git a/certbot-ci/certbot_integration_tests/conftest.py b/certbot-ci/certbot_integration_tests/conftest.py index e84a866b9..6d7b65e03 100644 --- a/certbot-ci/certbot_integration_tests/conftest.py +++ b/certbot-ci/certbot_integration_tests/conftest.py @@ -86,7 +86,7 @@ def _setup_primary_node(config): # By calling setup_acme_server we ensure that all necessary acme server instances will be # fully started. This runtime is reflected by the acme_xdist returned. - acme_server = acme_lib.setup_acme_server(config.option.acme_server, workers) + acme_server = acme_lib.ACMEServer(config.option.acme_server, workers) config.add_cleanup(acme_server.stop) print('ACME xdist config:\n{0}'.format(acme_server.acme_xdist)) acme_server.start() diff --git a/certbot-ci/certbot_integration_tests/utils/acme_server.py b/certbot-ci/certbot_integration_tests/utils/acme_server.py index 930755fc7..b50a2a38c 100755 --- a/certbot-ci/certbot_integration_tests/utils/acme_server.py +++ b/certbot-ci/certbot_integration_tests/utils/acme_server.py @@ -11,22 +11,70 @@ from os.path import join import requests import json -import yaml -from certbot_integration_tests.utils import misc +from certbot_integration_tests.utils import misc, pebble_artifacts from certbot_integration_tests.utils.constants import * class ACMEServer(object): """ - Handler exposing methods to start and stop the ACME server, and get its configuration - (eg. challenges ports). ACMEServer is also a context manager, and so can be used to - ensure ACME server is started/stopped upon context enter/exit. + ACMEServer configure and handle lifecycle of an ACME CA server and an HTTP reverse proxy + instance, to allow parallel execution of integration tests against the unique http-01 port + expected by the ACME CA server. + Typically all pytest integration tests will be executed in this context. + ACMEServer gives access the acme_xdist parameter, listing the ports and directory url to use + for each pytest node. It exposes also start and stop methods in order to start the stack, and + stop it with proper resources cleanup. + An ACMEServer instance will be returned, giving access to the ports and directory url to use + for each pytest node, and its start and stop methods are appropriately configured to + respectively start the server, and stop it with proper resources cleanup. + ACMEServer is also a context manager, and so can be used to ensure ACME server is started/stopped + upon context enter/exit. """ - def __init__(self, acme_xdist, start, stop): - self.acme_xdist = acme_xdist - self.start = start - self.stop = stop + def __init__(self, acme_server, nodes, proxy=True): + """ + Create an ACMEServer instance. + :param acme_server: the type of acme server used (boulder-v1, boulder-v2 or pebble) + :param str[] nodes: list of node names that will be setup by pytest xdist + :param bool proxy: set to False to not start the Traefik proxy + """ + self.acme_xdist = _construct_acme_xdist(acme_server, nodes) + + self._acme_type = 'pebble' if acme_server == 'pebble' else 'boulder' + self._proxy = proxy + self._workspace = tempfile.mkdtemp() + self._processes = [] + + def start(self): + """Start the test stack""" + if self._proxy: + self._processes.extend(_prepare_traefik_proxy(self._workspace, self.acme_xdist)) + if self._acme_type == 'pebble': + self._processes.extend(_prepare_pebble_server(self._workspace, self.acme_xdist)) + if self._acme_type == 'boulder': + self._processes.extend(_prepare_boulder_server(self._workspace, self.acme_xdist)) + + def stop(self): + """Stop the test stack, and clean its resources""" + print('=> Tear down the test infrastructure...') + try: + for process in self._processes: + process.terminate() + for process in self._processes: + process.wait() + + if os.path.exists(os.path.join(self._workspace, 'boulder')): + # Boulder docker generates build artifacts owned by root with 0o744 permissions. + # If we started the acme server from a normal user that has access to the Docker + # daemon, this user will not be able to delete these artifacts from the host. + # We need to do it through a docker. + process = _launch_process(['docker', 'run', '--rm', '-v', + '{0}:/workspace'.format(self._workspace), + 'alpine', 'rm', '-rf', '/workspace/boulder']) + process.wait() + finally: + shutil.rmtree(self._workspace) + print('=> Test infrastructure stopped and cleaned up.') def __enter__(self): self.start() @@ -36,32 +84,6 @@ class ACMEServer(object): self.stop() -def setup_acme_server(acme_server, nodes, proxy=True): - """ - This method will setup an ACME CA server and an HTTP reverse proxy instance, to allow parallel - execution of integration tests against the unique http-01 port expected by the ACME CA server. - Typically all pytest integration tests will be executed in this context. - An ACMEServer instance will be returned, giving access to the ports and directory url to use - for each pytest node, and its start and stop methods are appropriately configured to - respectively start the server, and stop it with proper resources cleanup. - :param str acme_server: the type of acme server used (boulder-v1, boulder-v2 or pebble) - :param str[] nodes: list of node names that will be setup by pytest xdist - :param bool proxy: set to False to not start the Traefik proxy - :return: a properly configured ACMEServer instance - :rtype: ACMEServer - """ - acme_type = 'pebble' if acme_server == 'pebble' else 'boulder' - acme_xdist = _construct_acme_xdist(acme_server, nodes) - workspace, stop = _construct_workspace(acme_type) - - def start(): - if proxy: - _prepare_traefik_proxy(workspace, acme_xdist) - _prepare_acme_server(workspace, acme_type, acme_xdist) - - return ACMEServer(acme_xdist, start, stop) - - def _construct_acme_xdist(acme_server, nodes): """Generate and return the acme_xdist dict""" acme_xdist = {'acme_server': acme_server, 'challtestsrv_port': CHALLTESTSRV_PORT} @@ -83,76 +105,62 @@ def _construct_acme_xdist(acme_server, nodes): return acme_xdist -def _construct_workspace(acme_type): - """Create a temporary workspace for integration tests stack""" - workspace = tempfile.mkdtemp() +def _prepare_pebble_server(workspace, acme_xdist): + print('=> Starting pebble instance deployment...') + pebble_path, challtestsrv_path, pebble_config_path = pebble_artifacts.fetch(workspace) - def cleanup(): - """Cleanup function to call that will teardown relevant dockers and their configuration.""" - for instance in [acme_type, 'traefik']: - print('=> Tear down the {0} instance...'.format(instance)) - instance_path = join(workspace, instance) - try: - if os.path.isfile(join(instance_path, 'docker-compose.yml')): - _launch_command(['docker-compose', 'down'], cwd=instance_path) - except subprocess.CalledProcessError: - pass - print('=> Finished tear down of {0} instance.'.format(acme_type)) + # Configure Pebble at full speed (PEBBLE_VA_NOSLEEP=1) and not randomly refusing valid + # nonce (PEBBLE_WFE_NONCEREJECT=0) to have a stable test environment. + environ = os.environ.copy() + environ['PEBBLE_VA_NOSLEEP'] = '1' + environ['PEBBLE_WFE_NONCEREJECT'] = '0' - if acme_type == 'boulder' and os.path.exists(os.path.join(workspace, 'boulder')): - # Boulder docker generates build artifacts owned by root user with 0o744 permissions. - # If we started the acme server from a normal user that has access to the Docker - # daemon, this user will not be able to delete these artifacts from the host. - # We need to do it through a docker. - _launch_command(['docker', 'run', '--rm', '-v', '{0}:/workspace'.format(workspace), - 'alpine', 'rm', '-rf', '/workspace/boulder']) + process_pebble = _launch_process( + [pebble_path, '-config', pebble_config_path, '-strict', '-dnsserver', '127.0.0.1:8053'], + env=environ) - shutil.rmtree(workspace) + process_challtestsrv = _launch_process( + [challtestsrv_path, '-management', ':{0}'.format(CHALLTESTSRV_PORT), '-defaultIPv6', '""', + '-defaultIPv4', '127.0.0.1', '-http01', '""', '-tlsalpn01', '""', '-https01', '""']) - return workspace, cleanup + # Wait for the ACME CA server to be up. + print('=> Waiting for pebble instance to respond...') + misc.check_until_timeout(acme_xdist['directory_url']) + + print('=> Finished pebble instance deployment.') + + return [process_pebble, process_challtestsrv] -def _prepare_acme_server(workspace, acme_type, acme_xdist): +def _prepare_boulder_server(workspace, acme_xdist): """Configure and launch the ACME server, Boulder or Pebble""" - print('=> Starting {0} instance deployment...'.format(acme_type)) - instance_path = join(workspace, acme_type) - try: - # Load Boulder/Pebble from git, that includes a docker-compose.yml ready for production. - _launch_command(['git', 'clone', 'https://github.com/letsencrypt/{0}'.format(acme_type), - '--single-branch', '--depth=1', instance_path]) - if acme_type == 'boulder': - # Allow Boulder to ignore usual limit rate policies, useful for tests. - os.rename(join(instance_path, 'test/rate-limit-policies-b.yml'), - join(instance_path, 'test/rate-limit-policies.yml')) - if acme_type == 'pebble': - # Configure Pebble at full speed (PEBBLE_VA_NOSLEEP=1) and not randomly refusing valid - # nonce (PEBBLE_WFE_NONCEREJECT=0) to have a stable test environment. - with open(os.path.join(instance_path, 'docker-compose.yml'), 'r') as file_handler: - config = yaml.load(file_handler.read()) + print('=> Starting boulder instance deployment...') + instance_path = join(workspace, 'boulder') - config['services']['pebble'].setdefault('environment', [])\ - .extend(['PEBBLE_VA_NOSLEEP=1', 'PEBBLE_WFE_NONCEREJECT=0']) - with open(os.path.join(instance_path, 'docker-compose.yml'), 'w') as file_handler: - file_handler.write(yaml.dump(config)) + # Load Boulder from git, that includes a docker-compose.yml ready for production. + process = _launch_process(['git', 'clone', 'https://github.com/letsencrypt/boulder', + '--single-branch', '--depth=1', instance_path]) + process.wait() - # Launch the ACME CA server. - _launch_command(['docker-compose', 'up', '--force-recreate', '-d'], cwd=instance_path) + # Allow Boulder to ignore usual limit rate policies, useful for tests. + os.rename(join(instance_path, 'test/rate-limit-policies-b.yml'), + join(instance_path, 'test/rate-limit-policies.yml')) - # Wait for the ACME CA server to be up. - print('=> Waiting for {0} instance to respond...'.format(acme_type)) - misc.check_until_timeout(acme_xdist['directory_url']) + # Launch the Boulder server + process = _launch_process(['docker-compose', 'up', '--force-recreate'], cwd=instance_path) - # Configure challtestsrv to answer any A record request with ip of the docker host. - acme_subnet = '10.77.77' if acme_type == 'boulder' else '10.30.50' - response = requests.post('http://localhost:{0}/set-default-ipv4' - .format(acme_xdist['challtestsrv_port']), - json={'ip': '{0}.1'.format(acme_subnet)}) - response.raise_for_status() + # Wait for the ACME CA server to be up. + print('=> Waiting for boulder instance to respond...') + misc.check_until_timeout(acme_xdist['directory_url']) - print('=> Finished {0} instance deployment.'.format(acme_type)) - except BaseException: - print('Error while setting up {0} instance.'.format(acme_type)) - raise + # Configure challtestsrv to answer any A record request with ip of the docker host. + response = requests.post('http://localhost:{0}/set-default-ipv4'.format(CHALLTESTSRV_PORT), + json={'ip': '10.77.77.1'}) + response.raise_for_status() + + print('=> Finished boulder instance deployment.') + + return [process] def _prepare_traefik_proxy(workspace, acme_xdist): @@ -185,7 +193,7 @@ networks: traefik_api_port=TRAEFIK_API_PORT, http_01_port=HTTP_01_PORT)) - _launch_command(['docker-compose', 'up', '--force-recreate', '-d'], cwd=instance_path) + process = _launch_process(['docker-compose', 'up', '--force-recreate'], cwd=instance_path) misc.check_until_timeout('http://localhost:{0}/api'.format(TRAEFIK_API_PORT)) config = { @@ -206,18 +214,19 @@ networks: response.raise_for_status() print('=> Finished traefik instance deployment.') + + return [process] except BaseException: print('Error while setting up traefik instance.') raise -def _launch_command(command, cwd=os.getcwd()): - """Launch silently an OS command, output will be displayed in case of failure""" - try: - subprocess.check_output(command, stderr=subprocess.STDOUT, cwd=cwd, universal_newlines=True) - except subprocess.CalledProcessError as e: - sys.stderr.write(e.output) - raise +def _launch_process(command, cwd=os.getcwd(), env=None): + """Launch silently an subprocess OS command""" + if not env: + env = os.environ + with open(os.devnull, 'w') as null: + return subprocess.Popen(command, stdout=null, stderr=subprocess.STDOUT, cwd=cwd, env=env) def main(): @@ -228,7 +237,7 @@ def main(): raise ValueError('Invalid server value {0}, should be one of {1}' .format(server_type, possible_values)) - acme_server = setup_acme_server(server_type, [], False) + acme_server = ACMEServer(server_type, [], False) process = None try: diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py new file mode 100644 index 000000000..bc7711e75 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py @@ -0,0 +1,52 @@ +import json +import platform +import os +import stat + +import pkg_resources +import requests + +PEBBLE_VERSION = 'v2.0.3' +ASSETS_PATH = pkg_resources.resource_filename('certbot_integration_tests', 'assets') + + +def fetch(workspace): + suffix = '{0}-{1}{2}'.format(platform.system().lower(), + platform.machine().lower().replace('x86_64', 'amd64'), + '.exe' if platform.system() == 'Windows' else '') + + pebble_path = _fetch_asset('pebble', suffix) + challtestsrv_path = _fetch_asset('challtestsrv', suffix) + pebble_config_path = _build_pebble_config(workspace) + + return pebble_path, challtestsrv_path, pebble_config_path + + +def _fetch_asset(asset, suffix): + asset_path = os.path.join(ASSETS_PATH, '{0}_{1}_{2}'.format(asset, PEBBLE_VERSION, suffix)) + if not os.path.exists(asset_path): + asset_url = ('https://github.com/adferrand/pebble/releases/download/{0}/{1}_{2}' + .format(PEBBLE_VERSION, asset, suffix)) + response = requests.get(asset_url) + response.raise_for_status() + with open(asset_path, 'wb') as file_h: + file_h.write(response.content) + os.chmod(asset_path, os.stat(asset_path).st_mode | stat.S_IEXEC) + + return asset_path + + +def _build_pebble_config(workspace): + config_path = os.path.join(workspace, 'pebble-config.json') + with open(config_path, 'w') as file_h: + file_h.write(json.dumps({ + 'pebble': { + 'listenAddress': '0.0.0.0:14000', + 'certificate': os.path.join(ASSETS_PATH, 'cert.pem'), + 'privateKey': os.path.join(ASSETS_PATH, 'key.pem'), + 'httpPort': 5002, + 'tlsPort': 5001, + }, + })) + + return config_path From a38ce67b084f8c4cd58bd784df14e9491de92a03 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sat, 15 Jun 2019 00:30:33 +0200 Subject: [PATCH 2/9] Fix nginx cert path --- .../certbot_integration_tests/nginx_tests/nginx_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py b/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py index a6305c3cc..18991ae62 100644 --- a/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py +++ b/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py @@ -21,9 +21,9 @@ def construct_nginx_config(nginx_root, nginx_webroot, http_port, https_port, oth :rtype: str """ key_path = key_path if key_path \ - else pkg_resources.resource_filename('certbot_integration_tests', 'assets/nginx_key.pem') + else pkg_resources.resource_filename('certbot_integration_tests', 'assets/key.pem') cert_path = cert_path if cert_path \ - else pkg_resources.resource_filename('certbot_integration_tests', 'assets/nginx_cert.pem') + else pkg_resources.resource_filename('certbot_integration_tests', 'assets/cert.pem') return '''\ # This error log will be written regardless of server scope error_log # definitions, so we have to set this here in the main scope. From f84ba87124afae1141594cf48415a8a066e4f5f7 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sat, 15 Jun 2019 00:07:53 +0200 Subject: [PATCH 3/9] Check conditionnally docker --- .../certbot_integration_tests/conftest.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/certbot-ci/certbot_integration_tests/conftest.py b/certbot-ci/certbot_integration_tests/conftest.py index 6d7b65e03..d52e4fb58 100644 --- a/certbot-ci/certbot_integration_tests/conftest.py +++ b/certbot-ci/certbot_integration_tests/conftest.py @@ -68,17 +68,18 @@ def _setup_primary_node(config): :param config: Configuration of the pytest primary node """ # Check for runtime compatibility: some tools are required to be available in PATH - try: - subprocess.check_output(['docker', '-v'], stderr=subprocess.STDOUT) - except (subprocess.CalledProcessError, OSError): - raise ValueError('Error: docker is required in PATH to launch the integration tests, ' - 'but is not installed or not available for current user.') + if 'boulder' in config.option.acme_server: + try: + subprocess.check_output(['docker', '-v'], stderr=subprocess.STDOUT) + except (subprocess.CalledProcessError, OSError): + raise ValueError('Error: docker is required in PATH to launch the integration tests on' + 'boulder, but is not installed or not available for current user.') - try: - subprocess.check_output(['docker-compose', '-v'], stderr=subprocess.STDOUT) - except (subprocess.CalledProcessError, OSError): - raise ValueError('Error: docker-compose is required in PATH to launch the integration tests, ' - 'but is not installed or not available for current user.') + try: + subprocess.check_output(['docker-compose', '-v'], stderr=subprocess.STDOUT) + except (subprocess.CalledProcessError, OSError): + raise ValueError('Error: docker-compose is required in PATH to launch the integration tests, ' + 'but is not installed or not available for current user.') # Parameter numprocesses is added to option by pytest-xdist workers = ['primary'] if not config.option.numprocesses\ From c26723e54870d29fb6b1c849ae1a7df2f62d1ca5 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sat, 15 Jun 2019 01:02:57 +0200 Subject: [PATCH 4/9] Update gitignore, fix apacheconftest --- .gitignore | 2 ++ .../tests/apache-conf-files/apache-conf-test-pebble.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 56ec23fbd..35771600e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,5 @@ tests/letstest/venv/ # certbot tests .certbot_test_workspace +**/assets/pebble* +**/assets/challtestsrv* \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test-pebble.py b/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test-pebble.py index 34f32f2d7..68bd6287d 100755 --- a/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test-pebble.py +++ b/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test-pebble.py @@ -15,7 +15,7 @@ SCRIPT_DIRNAME = os.path.dirname(__file__) def main(args=None): if not args: args = sys.argv[1:] - with acme_server.setup_acme_server('pebble', [], False) as acme_xdist: + with acme_server.ACMEServer('pebble', [], False) as acme_xdist: environ = os.environ.copy() environ['SERVER'] = acme_xdist['directory_url'] command = [os.path.join(SCRIPT_DIRNAME, 'apache-conf-test')] From d445b96442d8dd9b1dbbc6f6ef868f2b0a80d74e Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Mon, 17 Jun 2019 09:39:17 +0200 Subject: [PATCH 5/9] Full object --- .../utils/acme_server.py | 166 +++++++++--------- 1 file changed, 79 insertions(+), 87 deletions(-) diff --git a/certbot-ci/certbot_integration_tests/utils/acme_server.py b/certbot-ci/certbot_integration_tests/utils/acme_server.py index ca53e7acd..21d1b7c8f 100755 --- a/certbot-ci/certbot_integration_tests/utils/acme_server.py +++ b/certbot-ci/certbot_integration_tests/utils/acme_server.py @@ -38,7 +38,7 @@ class ACMEServer(object): :param str[] nodes: list of node names that will be setup by pytest xdist :param bool proxy: set to False to not start the Traefik proxy """ - self.acme_xdist = _construct_acme_xdist(acme_server, nodes) + self._construct_acme_xdist(acme_server, nodes) self._acme_type = 'pebble' if acme_server == 'pebble' else 'boulder' self._proxy = proxy @@ -48,11 +48,11 @@ class ACMEServer(object): def start(self): """Start the test stack""" if self._proxy: - self._processes.extend(_prepare_http_proxy(self.acme_xdist)) + self._prepare_http_proxy() if self._acme_type == 'pebble': - self._processes.extend(_prepare_pebble_server(self._workspace, self.acme_xdist)) + self._prepare_pebble_server() if self._acme_type == 'boulder': - self._processes.extend(_prepare_boulder_server(self._workspace, self.acme_xdist)) + self._prepare_boulder_server() def stop(self): """Stop the test stack, and clean its resources""" @@ -68,9 +68,9 @@ class ACMEServer(object): # If we started the acme server from a normal user that has access to the Docker # daemon, this user will not be able to delete these artifacts from the host. # We need to do it through a docker. - process = _launch_process(['docker', 'run', '--rm', '-v', - '{0}:/workspace'.format(self._workspace), - 'alpine', 'rm', '-rf', '/workspace/boulder']) + process = self._launch_process(['docker', 'run', '--rm', '-v', + '{0}:/workspace'.format(self._workspace), + 'alpine', 'rm', '-rf', '/workspace/boulder']) process.wait() finally: shutil.rmtree(self._workspace) @@ -83,104 +83,96 @@ class ACMEServer(object): def __exit__(self, exc_type, exc_val, exc_tb): self.stop() + def _construct_acme_xdist(self, acme_server, nodes): + """Generate and return the acme_xdist dict""" + acme_xdist = {'acme_server': acme_server, 'challtestsrv_port': CHALLTESTSRV_PORT} -def _construct_acme_xdist(acme_server, nodes): - """Generate and return the acme_xdist dict""" - acme_xdist = {'acme_server': acme_server, 'challtestsrv_port': CHALLTESTSRV_PORT} + # Directory and ACME port are set implicitly in the docker-compose.yml files of Boulder/Pebble. + if acme_server == 'pebble': + acme_xdist['directory_url'] = PEBBLE_DIRECTORY_URL + else: # boulder + acme_xdist['directory_url'] = BOULDER_V2_DIRECTORY_URL \ + if acme_server == 'boulder-v2' else BOULDER_V1_DIRECTORY_URL - # Directory and ACME port are set implicitly in the docker-compose.yml files of Boulder/Pebble. - if acme_server == 'pebble': - acme_xdist['directory_url'] = PEBBLE_DIRECTORY_URL - else: # boulder - acme_xdist['directory_url'] = BOULDER_V2_DIRECTORY_URL \ - if acme_server == 'boulder-v2' else BOULDER_V1_DIRECTORY_URL + acme_xdist['http_port'] = {node: port for (node, port) + in zip(nodes, range(5200, 5200 + len(nodes)))} + acme_xdist['https_port'] = {node: port for (node, port) + in zip(nodes, range(5100, 5100 + len(nodes)))} + acme_xdist['other_port'] = {node: port for (node, port) + in zip(nodes, range(5300, 5300 + len(nodes)))} - acme_xdist['http_port'] = {node: port for (node, port) - in zip(nodes, range(5200, 5200 + len(nodes)))} - acme_xdist['https_port'] = {node: port for (node, port) - in zip(nodes, range(5100, 5100 + len(nodes)))} - acme_xdist['other_port'] = {node: port for (node, port) - in zip(nodes, range(5300, 5300 + len(nodes)))} + self.acme_xdist = acme_xdist - return acme_xdist + def _prepare_pebble_server(self): + """Configure and launch the Pebble server""" + print('=> Starting pebble instance deployment...') + pebble_path, challtestsrv_path, pebble_config_path = pebble_artifacts.fetch(self._workspace) + # Configure Pebble at full speed (PEBBLE_VA_NOSLEEP=1) and not randomly refusing valid + # nonce (PEBBLE_WFE_NONCEREJECT=0) to have a stable test environment. + environ = os.environ.copy() + environ['PEBBLE_VA_NOSLEEP'] = '1' + environ['PEBBLE_WFE_NONCEREJECT'] = '0' -def _prepare_pebble_server(workspace, acme_xdist): - print('=> Starting pebble instance deployment...') - pebble_path, challtestsrv_path, pebble_config_path = pebble_artifacts.fetch(workspace) + self._launch_process( + [pebble_path, '-config', pebble_config_path, '-strict', '-dnsserver', '127.0.0.1:8053'], + env=environ) - # Configure Pebble at full speed (PEBBLE_VA_NOSLEEP=1) and not randomly refusing valid - # nonce (PEBBLE_WFE_NONCEREJECT=0) to have a stable test environment. - environ = os.environ.copy() - environ['PEBBLE_VA_NOSLEEP'] = '1' - environ['PEBBLE_WFE_NONCEREJECT'] = '0' + self._launch_process( + [challtestsrv_path, '-management', ':{0}'.format(CHALLTESTSRV_PORT), '-defaultIPv6', '""', + '-defaultIPv4', '127.0.0.1', '-http01', '""', '-tlsalpn01', '""', '-https01', '""']) - process_pebble = _launch_process( - [pebble_path, '-config', pebble_config_path, '-strict', '-dnsserver', '127.0.0.1:8053'], - env=environ) + # Wait for the ACME CA server to be up. + print('=> Waiting for pebble instance to respond...') + misc.check_until_timeout(self.acme_xdist['directory_url']) - process_challtestsrv = _launch_process( - [challtestsrv_path, '-management', ':{0}'.format(CHALLTESTSRV_PORT), '-defaultIPv6', '""', - '-defaultIPv4', '127.0.0.1', '-http01', '""', '-tlsalpn01', '""', '-https01', '""']) + print('=> Finished pebble instance deployment.') - # Wait for the ACME CA server to be up. - print('=> Waiting for pebble instance to respond...') - misc.check_until_timeout(acme_xdist['directory_url']) + def _prepare_boulder_server(self): + """Configure and launch the Boulder server""" + print('=> Starting boulder instance deployment...') + instance_path = join(self._workspace, 'boulder') - print('=> Finished pebble instance deployment.') + # Load Boulder from git, that includes a docker-compose.yml ready for production. + process = self._launch_process(['git', 'clone', 'https://github.com/letsencrypt/boulder', + '--single-branch', '--depth=1', instance_path]) + process.wait() - return [process_pebble, process_challtestsrv] + # Allow Boulder to ignore usual limit rate policies, useful for tests. + os.rename(join(instance_path, 'test/rate-limit-policies-b.yml'), + join(instance_path, 'test/rate-limit-policies.yml')) + # Launch the Boulder server + self._launch_process(['docker-compose', 'up', '--force-recreate'], cwd=instance_path) -def _prepare_boulder_server(workspace, acme_xdist): - """Configure and launch the ACME server, Boulder or Pebble""" - print('=> Starting boulder instance deployment...') - instance_path = join(workspace, 'boulder') + # Wait for the ACME CA server to be up. + print('=> Waiting for boulder instance to respond...') + misc.check_until_timeout(self.acme_xdist['directory_url']) - # Load Boulder from git, that includes a docker-compose.yml ready for production. - process = _launch_process(['git', 'clone', 'https://github.com/letsencrypt/boulder', - '--single-branch', '--depth=1', instance_path]) - process.wait() + # Configure challtestsrv to answer any A record request with ip of the docker host. + response = requests.post('http://localhost:{0}/set-default-ipv4'.format(CHALLTESTSRV_PORT), + json={'ip': '10.77.77.1'}) + response.raise_for_status() - # Allow Boulder to ignore usual limit rate policies, useful for tests. - os.rename(join(instance_path, 'test/rate-limit-policies-b.yml'), - join(instance_path, 'test/rate-limit-policies.yml')) + print('=> Finished boulder instance deployment.') - # Launch the Boulder server - process = _launch_process(['docker-compose', 'up', '--force-recreate'], cwd=instance_path) + def _prepare_http_proxy(self): + """Configure and launch an HTTP proxy""" + print('=> Configuring the HTTP proxy...') + mapping = {r'.+\.{0}\.wtf'.format(node): 'http://127.0.0.1:{0}'.format(port) + for node, port in self.acme_xdist['http_port'].items()} + command = [sys.executable, proxy.__file__, str(HTTP_01_PORT), json.dumps(mapping)] + self._launch_process(command) + print('=> Finished configuring the HTTP proxy.') - # Wait for the ACME CA server to be up. - print('=> Waiting for boulder instance to respond...') - misc.check_until_timeout(acme_xdist['directory_url']) - - # Configure challtestsrv to answer any A record request with ip of the docker host. - response = requests.post('http://localhost:{0}/set-default-ipv4'.format(CHALLTESTSRV_PORT), - json={'ip': '10.77.77.1'}) - response.raise_for_status() - - print('=> Finished boulder instance deployment.') - - return [process] - - -def _prepare_http_proxy(acme_xdist): - """Configure and launch an HTTP proxy""" - print('=> Configuring the HTTP proxy...') - mapping = {r'.+\.{0}\.wtf'.format(node): 'http://127.0.0.1:{0}'.format(port) - for node, port in acme_xdist['http_port'].items()} - command = [sys.executable, proxy.__file__, str(HTTP_01_PORT), json.dumps(mapping)] - process = _launch_process(command) - print('=> Finished configuring the HTTP proxy.') - - return [process] - - -def _launch_process(command, cwd=os.getcwd(), env=None): - """Launch silently an subprocess OS command""" - if not env: - env = os.environ - with open(os.devnull, 'w') as null: - return subprocess.Popen(command, stdout=null, stderr=subprocess.STDOUT, cwd=cwd, env=env) + def _launch_process(self, command, cwd=os.getcwd(), env=None): + """Launch silently an subprocess OS command""" + if not env: + env = os.environ + with open(os.devnull, 'w') as null: + process = subprocess.Popen(command, stdout=null, stderr=subprocess.STDOUT, cwd=cwd, env=env) + self._processes.append(process) + return process def main(): From 699c1540fb16221cae998d1bcd841dda7b85bc80 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Mon, 17 Jun 2019 09:41:34 +0200 Subject: [PATCH 6/9] Carriage return --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 35771600e..68762da6b 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,4 @@ tests/letstest/venv/ # certbot tests .certbot_test_workspace **/assets/pebble* -**/assets/challtestsrv* \ No newline at end of file +**/assets/challtestsrv* From 2939dde0291b90fcfb7888f2b8f6ab70e2cee21b Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Fri, 21 Jun 2019 20:57:00 +0200 Subject: [PATCH 7/9] Move to official v2.1.0 of pebble --- .../certbot_integration_tests/utils/pebble_artifacts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py index bc7711e75..286260e09 100644 --- a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py +++ b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py @@ -6,7 +6,7 @@ import stat import pkg_resources import requests -PEBBLE_VERSION = 'v2.0.3' +PEBBLE_VERSION = 'v2.1.0' ASSETS_PATH = pkg_resources.resource_filename('certbot_integration_tests', 'assets') @@ -25,7 +25,7 @@ def fetch(workspace): def _fetch_asset(asset, suffix): asset_path = os.path.join(ASSETS_PATH, '{0}_{1}_{2}'.format(asset, PEBBLE_VERSION, suffix)) if not os.path.exists(asset_path): - asset_url = ('https://github.com/adferrand/pebble/releases/download/{0}/{1}_{2}' + asset_url = ('https://github.com/letsencrypt/pebble/releases/download/{0}/{1}_{2}' .format(PEBBLE_VERSION, asset, suffix)) response = requests.get(asset_url) response.raise_for_status() From 2d0eafbec0b217f1320a2b56b2a9fedaab6e92d3 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Fri, 21 Jun 2019 21:15:10 +0200 Subject: [PATCH 8/9] Fix name --- certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py index 286260e09..9f154bb0e 100644 --- a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py +++ b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py @@ -16,7 +16,7 @@ def fetch(workspace): '.exe' if platform.system() == 'Windows' else '') pebble_path = _fetch_asset('pebble', suffix) - challtestsrv_path = _fetch_asset('challtestsrv', suffix) + challtestsrv_path = _fetch_asset('pebble-challtestsrv', suffix) pebble_config_path = _build_pebble_config(workspace) return pebble_path, challtestsrv_path, pebble_config_path From 75d6414dcb8eea8f706203e3cdbe39d114364f87 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Fri, 21 Jun 2019 23:28:23 +0200 Subject: [PATCH 9/9] Update acme_server.py --- certbot-ci/certbot_integration_tests/utils/acme_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-ci/certbot_integration_tests/utils/acme_server.py b/certbot-ci/certbot_integration_tests/utils/acme_server.py index 21d1b7c8f..2b50f3096 100755 --- a/certbot-ci/certbot_integration_tests/utils/acme_server.py +++ b/certbot-ci/certbot_integration_tests/utils/acme_server.py @@ -115,7 +115,7 @@ class ACMEServer(object): environ['PEBBLE_WFE_NONCEREJECT'] = '0' self._launch_process( - [pebble_path, '-config', pebble_config_path, '-strict', '-dnsserver', '127.0.0.1:8053'], + [pebble_path, '-config', pebble_config_path, '-dnsserver', '127.0.0.1:8053'], env=environ) self._launch_process(