mirror of
https://github.com/certbot/certbot.git
synced 2026-07-30 18:04:31 +02:00
Configuration File Update w/o Certificate Issuance (#9355)
* Add command to update config files without issuing/renewing cert * toss up a vague untested skeleton * remove duplicated code * set certname in config * consistent name, no zope * import copy * reconsitute is in renewal * import renewal * import cli * fix lint errors * call choose_configurator_plugins for its side effect of writing to config * Set certonly in choose config plugins as we do for renew * rewrite by piggybacking on existing side effects of a dry run instead * do not allow domains to be set while reconfiguring * remove unused cert_manager.reconfigure * remove unused imports * Add comments and messages * add cli information * start adding tests * remove test code * get certname before setting up plugins * get plugin from lineage if not set on cli * import copy * always reconstitute * only load cert once * add error message * improve comment * mock everything out for tests * test functionality is working! * add tests for adding and modifying hooks * test that we don't modify the config if the dry run fails * improve documentation * add webroot to reconfigure common options * lint and clean up intermediate artifacts * mock validate_hooks for windows * print success message with updated parameters * Improve success message * add message for no changes have been made * improve changed message to show before as well * syntax * Add changes will apply at the next renewal message * lint * lint really likes dict.items() for some reason * run the deploy hook * turn off dry run to test deploy hook * patch list_hooks call for tests * factor out reporting results code * Remove reporting of which values were changed * add flag to run deploy hook despite doing a dry run, and recommend setting that to yes when running reconfigure and modifying the deploy hook * missing () around multi-line string * test if the two dicts are equal instead of finding the actual changes, thus avoiding having to deal with webroot_map being a list * refer to --deploy-hook instead of deploy hook * use renewal configuration instead of configuration information * mention that the deploy hook will use the active cert not the test one * disable lint and remove new from language asking about running a deploy hook * pluralize run deploy hook(s) * Add test for reporting results when there is a webroot map * update changelog * Update error message about modifying domains on the certificate * update changelog * Add basic integration tests * Just set -a rather than redoing the whole testing infrastructure * used webroot in integration test since it's already installed * file contents are accessed twice now --------- Co-authored-by: Alex Zorin <alex@zorin.au>
This commit is contained in:
+163
-1
@@ -14,6 +14,7 @@ from typing import List
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import configobj
|
||||
import josepy as jose
|
||||
import pytz
|
||||
|
||||
@@ -530,6 +531,167 @@ class RevokeTest(test_util.TempDirTestCase):
|
||||
self._call()
|
||||
self.assertIs(mock_delete.called, False)
|
||||
|
||||
|
||||
class ReconfigureTest(test_util.TempDirTestCase):
|
||||
"""Tests for certbot._internal.main.reconfigure"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.get_utility_patch = test_util.patch_display_util()
|
||||
self.mock_get_utility = self.get_utility_patch.start()
|
||||
self.patchers = {
|
||||
'check_symlinks': mock.patch('certbot._internal.storage.RenewableCert._check_symlinks'),
|
||||
'cert_names': mock.patch('certbot._internal.storage.RenewableCert.names'),
|
||||
'pick_installer': mock.patch('certbot._internal.plugins.selection.pick_installer'),
|
||||
'pick_auth': mock.patch('certbot._internal.plugins.selection.pick_authenticator'),
|
||||
'find_init': mock.patch('certbot._internal.plugins.disco.PluginsRegistry.find_init'),
|
||||
'_get_and_save_cert': mock.patch('certbot._internal.main._get_and_save_cert'),
|
||||
'_init_le_client': mock.patch('certbot._internal.main._init_le_client'),
|
||||
'list_hooks': mock.patch('certbot._internal.hooks.list_hooks'),
|
||||
}
|
||||
self.mocks = {k: v.start() for k, v in self.patchers.items()}
|
||||
self.mocks['cert_names'].return_value = ['example.com']
|
||||
|
||||
self.config_dir = os.path.join(self.tempdir, 'config')
|
||||
renewal_configs_dir = os.path.join(self.config_dir, 'renewal')
|
||||
if not os.path.exists(renewal_configs_dir):
|
||||
filesystem.makedirs(renewal_configs_dir)
|
||||
self.renewal_file = os.path.join(renewal_configs_dir, 'example.com.conf')
|
||||
original_config = """
|
||||
version = 1.32.0
|
||||
archive_dir = /etc/letsencrypt/archive/example.com
|
||||
cert = /etc/letsencrypt/live/example.com/cert.pem
|
||||
privkey = /etc/letsencrypt/live/example.com/privkey.pem
|
||||
chain = /etc/letsencrypt/live/example.com/chain.pem
|
||||
fullchain = /etc/letsencrypt/live/example.com/fullchain.pem
|
||||
|
||||
# Options used in the renewal process
|
||||
[renewalparams]
|
||||
account = ee43634db0aa4e6804f152be39990e6a
|
||||
server = https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
authenticator = nginx
|
||||
installer = nginx
|
||||
key_type = rsa
|
||||
"""
|
||||
with open(self.renewal_file, 'w') as f:
|
||||
f.write(original_config)
|
||||
with open(self.renewal_file, 'r') as f:
|
||||
self.original_config = configobj.ConfigObj(f,
|
||||
encoding='utf-8', default_encoding='utf-8')
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.get_utility_patch.stop()
|
||||
for patch in self.patchers.values():
|
||||
patch.stop()
|
||||
|
||||
def _call(self, passed_args):
|
||||
full_args = passed_args + ['--config-dir', self.config_dir]
|
||||
cli.set_by_cli.detector = None # required to reset set_by_cli state
|
||||
plugins = disco.PluginsRegistry.find_all()
|
||||
config = configuration.NamespaceConfig(
|
||||
cli.prepare_and_parse_args(plugins, full_args))
|
||||
|
||||
from certbot._internal.main import reconfigure
|
||||
reconfigure(config, plugins)
|
||||
|
||||
with open(self.renewal_file, 'r') as f:
|
||||
updated_conf = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8')
|
||||
|
||||
return updated_conf
|
||||
|
||||
def test_domains_set(self):
|
||||
self.assertRaises(errors.ConfigurationError,
|
||||
self._call, '--cert-name cert1 -d one.cert.com'.split())
|
||||
|
||||
@mock.patch('certbot._internal.cert_manager.get_certnames')
|
||||
def test_asks_for_certname(self, mock_cert_manager):
|
||||
mock_cert_manager.return_value = ['example.com']
|
||||
self._call('--nginx'.split())
|
||||
self.assertEqual(mock_cert_manager.call_count, 1)
|
||||
|
||||
def test_update_configurator(self):
|
||||
named_mock = mock.Mock()
|
||||
named_mock.name = 'apache'
|
||||
|
||||
self.mocks['pick_installer'].return_value = named_mock
|
||||
self.mocks['pick_auth'].return_value = named_mock
|
||||
self.mocks['find_init'].return_value = named_mock
|
||||
|
||||
new_config = self._call('--cert-name example.com --apache'.split())
|
||||
self.assertEqual(new_config['renewalparams']['authenticator'], 'apache')
|
||||
|
||||
@mock.patch('certbot._internal.hooks.validate_hooks')
|
||||
def test_update_hooks(self, unused_validate_hooks):
|
||||
self.assertNotIn('pre_hook', self.original_config)
|
||||
# test set
|
||||
new_config = self._call('--cert-name example.com --pre-hook'.split() + ['echo pre'])
|
||||
self.assertEqual(new_config['renewalparams']['pre_hook'], 'echo pre')
|
||||
# test update
|
||||
new_config = self._call('--cert-name example.com --pre-hook'.split() + ['echo pre2'])
|
||||
self.assertEqual(new_config['renewalparams']['pre_hook'], 'echo pre2')
|
||||
|
||||
# test deploy hook is set even though we did a dry run
|
||||
self.assertNotIn('renew_hook', self.original_config)
|
||||
new_config = self._call('--cert-name example.com --deploy-hook'.split() + ['echo deploy'])
|
||||
self.assertEqual(new_config['renewalparams']['renew_hook'], 'echo deploy')
|
||||
|
||||
def test_dry_run_fails(self):
|
||||
# set side effect of raising error
|
||||
self.mocks['_get_and_save_cert'].side_effect = errors.Error
|
||||
|
||||
try:
|
||||
self._call('--cert-name example.com --apache'.split())
|
||||
except errors.Error:
|
||||
pass
|
||||
|
||||
# check that config isn't modified
|
||||
with open(self.renewal_file, 'r') as f:
|
||||
new_config = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8')
|
||||
self.assertEqual(new_config['renewalparams']['authenticator'], 'nginx')
|
||||
|
||||
@mock.patch('certbot._internal.main.display_util.notify')
|
||||
def test_report_results(self, mock_notify):
|
||||
# make sure report results works when config has a webroot map
|
||||
original_config = """
|
||||
version = 2.0.0
|
||||
archive_dir = /etc/letsencrypt/archive/example.com
|
||||
cert = /etc/letsencrypt/live/example.com/cert.pem
|
||||
privkey = /etc/letsencrypt/live/example.com/privkey.pem
|
||||
chain = /etc/letsencrypt/live/example.com/chain.pem
|
||||
fullchain = /etc/letsencrypt/live/example.com/fullchain.pem
|
||||
|
||||
# Options used in the renewal process
|
||||
[renewalparams]
|
||||
account = ee43634db0aa4e6804f152be39990e6a
|
||||
server = https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
authenticator = webroot
|
||||
installer = nginx
|
||||
key_type = ecdsa
|
||||
webroot_path = /var/www/html,
|
||||
[[webroot_map]]
|
||||
example.com = /var/www/html
|
||||
"""
|
||||
with open(self.renewal_file, 'w') as f:
|
||||
f.write(original_config)
|
||||
with open(self.renewal_file, 'r') as f:
|
||||
self.original_config = configobj.ConfigObj(f,
|
||||
encoding='utf-8', default_encoding='utf-8')
|
||||
|
||||
named_mock = mock.Mock()
|
||||
named_mock.name = 'nginx'
|
||||
|
||||
self.mocks['pick_auth'].return_value = named_mock
|
||||
self.mocks['find_init'].return_value = named_mock
|
||||
|
||||
new_config = self._call('--cert-name example.com --nginx'.split())
|
||||
self.assertEqual(new_config['renewalparams']['authenticator'], 'nginx')
|
||||
mock_notify.assert_called_with(
|
||||
'\nSuccessfully updated configuration.'+
|
||||
'\nChanges will apply when the certificate renews.')
|
||||
|
||||
|
||||
class DeleteIfAppropriateTest(test_util.ConfigTestCase):
|
||||
"""Tests for certbot._internal.main._delete_if_appropriate """
|
||||
|
||||
@@ -1492,7 +1654,7 @@ class MainTest(test_util.ConfigTestCase):
|
||||
|
||||
def test_renew_reconstitute_error(self):
|
||||
# pylint: disable=protected-access
|
||||
with mock.patch('certbot._internal.main.renewal._reconstitute') as mock_reconstitute:
|
||||
with mock.patch('certbot._internal.main.renewal.reconstitute') as mock_reconstitute:
|
||||
mock_reconstitute.side_effect = Exception
|
||||
self._test_renew_common(assert_oc_called=False, error_expected=True)
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ class RenewalTest(test_util.ConfigTestCase):
|
||||
|
||||
from certbot._internal import renewal
|
||||
lineage_config = copy.deepcopy(self.config)
|
||||
renewal_candidate = renewal._reconstitute(lineage_config, rc_path)
|
||||
renewal_candidate = renewal.reconstitute(lineage_config, rc_path)
|
||||
# This means that manual_public_ip_logging_ok was not modified in the config based on its
|
||||
# value in the renewal conf file
|
||||
self.assertIsInstance(lineage_config.manual_public_ip_logging_ok, mock.MagicMock)
|
||||
|
||||
Reference in New Issue
Block a user