Skip to content
Snippets Groups Projects
Commit 10b74816 authored by Ted Bowman's avatar Ted Bowman Committed by Adam G-H
Browse files

Issue #3259664 by tedbow: Add non-UI config option to allow attended minor core updates

parent 042692d6
No related branches found
No related tags found
1 merge request!175Issue #3259664: Add non-UI config option to allow attended minor core updates
......@@ -47,6 +47,7 @@ services:
class: Drupal\automatic_updates\Validator\UpdateVersionValidator
arguments:
- '@string_translation'
- '@config.factory'
tags:
- { name: event_subscriber }
automatic_updates.composer_executable_validator:
......
cron: security
allow_core_minor_updates: false
......@@ -5,3 +5,6 @@ automatic_updates.settings:
cron:
type: string
label: 'Enable automatic updates during cron'
allow_core_minor_updates:
type: boolean
label: 'Allow minor level Drupal core updates'
......@@ -116,7 +116,7 @@ class CronUpdater extends Updater {
$this->destroy();
}
catch (StageValidationException $e) {
$this->logger->error($this->getLogMessageForValidationException($e));
$this->logger->error(static::formatValidationException($e));
return;
}
catch (\Throwable $e) {
......@@ -142,7 +142,7 @@ class CronUpdater extends Updater {
* @return string
* The formatted log message, including all the validation results.
*/
protected function getLogMessageForValidationException(StageValidationException $exception): string {
protected static function formatValidationException(StageValidationException $exception): string {
$log_message = '';
foreach ($exception->getResults() as $result) {
$summary = $result->getSummary();
......
......@@ -3,8 +3,10 @@
namespace Drupal\automatic_updates\Validator;
use Composer\Semver\Semver;
use Drupal\automatic_updates\CronUpdater;
use Drupal\automatic_updates\Event\ReadinessCheckEvent;
use Drupal\automatic_updates\Updater;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreOperationStageEvent;
use Drupal\Core\Extension\ExtensionVersion;
......@@ -19,14 +21,24 @@ class UpdateVersionValidator implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a UpdateVersionValidation object.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
* The translation service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
*/
public function __construct(TranslationInterface $translation) {
public function __construct(TranslationInterface $translation, ConfigFactoryInterface $config_factory) {
$this->setStringTranslation($translation);
$this->configFactory = $config_factory;
}
/**
......@@ -45,7 +57,7 @@ class UpdateVersionValidator implements EventSubscriberInterface {
}
/**
* Validates that core is not being updated to another minor or major version.
* Validates that core is being updated within an allowed version range.
*
* @param \Drupal\package_manager\Event\PreOperationStageEvent $event
* The event object.
......@@ -79,33 +91,36 @@ class UpdateVersionValidator implements EventSubscriberInterface {
$core_package_name = reset($core_package_names);
$to_version_string = $package_versions[$core_package_name];
$to_version = ExtensionVersion::createFromVersionString($to_version_string);
$variables = [
'@to_version' => $to_version_string,
'@from_version' => $from_version_string,
];
if (Semver::satisfies($to_version_string, "< $from_version_string")) {
$messages[] = $this->t('Update version @to_version is lower than @from_version, downgrading is not supported.', [
'@to_version' => $to_version_string,
'@from_version' => $from_version_string,
$event->addError([
$this->t('Update version @to_version is lower than @from_version, downgrading is not supported.', $variables),
]);
$event->addError($messages);
}
elseif ($from_version->getVersionExtra() === 'dev') {
$messages[] = $this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from a dev version to any other version are not supported.', [
'@to_version' => $to_version_string,
'@from_version' => $from_version_string,
$event->addError([
$this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from a dev version to any other version are not supported.', $variables),
]);
$event->addError($messages);
}
elseif ($from_version->getMajorVersion() !== $to_version->getMajorVersion()) {
$messages[] = $this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one major version to another are not supported.', [
'@to_version' => $to_version_string,
'@from_version' => $from_version_string,
$event->addError([
$this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one major version to another are not supported.', $variables),
]);
$event->addError($messages);
}
elseif ($from_version->getMinorVersion() !== $to_version->getMinorVersion()) {
$messages[] = $this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one minor version to another are not supported.', [
'@from_version' => $this->getCoreVersion(),
'@to_version' => $package_versions[$core_package_name],
]);
$event->addError($messages);
if (!$this->configFactory->get('automatic_updates.settings')->get('allow_core_minor_updates')) {
$event->addError([
$this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one minor version to another are not supported.', $variables),
]);
}
elseif ($stage instanceof CronUpdater) {
$event->addError([
$this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one minor version to another are not supported during cron.', $variables),
]);
}
}
}
......
......@@ -2,8 +2,10 @@
namespace Drupal\Tests\automatic_updates\Kernel;
use Drupal\automatic_updates\CronUpdater;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\KernelTestBase;
use Drupal\package_manager\Exception\StageValidationException;
use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
......@@ -124,3 +126,31 @@ abstract class AutomaticUpdatesKernelTestBase extends KernelTestBase {
}
}
/**
* A test-only version of the cron updater to expose internal methods.
*/
class TestCronUpdater extends CronUpdater {
/**
* The directory where staging areas will be created.
*
* @var string
*/
public static $stagingRoot;
/**
* {@inheritdoc}
*/
protected static function getStagingRoot(): string {
return static::$stagingRoot ?: parent::getStagingRoot();
}
/**
* {@inheritdoc}
*/
public static function formatValidationException(StageValidationException $exception): string {
return parent::formatValidationException($exception);
}
}
......@@ -2,11 +2,11 @@
namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation;
use Drupal\automatic_updates\CronUpdater;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\package_manager\Exception\StageValidationException;
use Drupal\package_manager\ValidationResult;
use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
use Drupal\Tests\automatic_updates\Kernel\TestCronUpdater;
/**
* @covers \Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator
......@@ -39,7 +39,7 @@ class StagedDatabaseUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
TestCronUpdater::$stagingRoot = $this->vfsRoot->url();
/** @var \Drupal\Tests\automatic_updates\Kernel\ReadinessValidation\TestCronUpdater $updater */
/** @var \Drupal\Tests\automatic_updates\Kernel\TestCronUpdater $updater */
$updater = $this->container->get('automatic_updates.cron_updater');
$updater->begin(['drupal' => '9.8.1']);
$updater->stage();
......@@ -184,24 +184,3 @@ class StagedDatabaseUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
}
}
/**
* A test-only version of the cron updater.
*/
class TestCronUpdater extends CronUpdater {
/**
* The directory where staging areas will be created.
*
* @var string
*/
public static $stagingRoot;
/**
* {@inheritdoc}
*/
protected static function getStagingRoot(): string {
return static::$stagingRoot ?: parent::getStagingRoot();
}
}
......@@ -2,8 +2,14 @@
namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation;
use Drupal\automatic_updates\CronUpdater;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\package_manager\Exception\StageValidationException;
use Drupal\package_manager\ValidationResult;
use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
use Drupal\Tests\automatic_updates\Kernel\TestCronUpdater;
use Drupal\Tests\package_manager\Traits\PackageManagerBypassTestTrait;
use Psr\Log\Test\TestLogger;
/**
* @covers \Drupal\automatic_updates\Validator\UpdateVersionValidator
......@@ -12,12 +18,15 @@ use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
*/
class UpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase {
use PackageManagerBypassTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'automatic_updates',
'package_manager',
'package_manager_bypass',
];
/**
......@@ -36,18 +45,117 @@ class UpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase {
'Drupal cannot be automatically updated from its current version, 8.9.1, to the recommended version, 9.8.1, because automatic updates from one major version to another are not supported.',
]);
$this->assertCheckerResultsFromManager([$result], TRUE);
}
/**
* Data provider for ::testMinorUpdates().
*
* @return array[]
* Sets of arguments to pass to the test method.
*/
public function providerMinorUpdates(): array {
$update_disallowed = ValidationResult::createError([
'Drupal cannot be automatically updated from its current version, 9.7.1, to the recommended version, 9.8.1, because automatic updates from one minor version to another are not supported.',
]);
$cron_update_disallowed = ValidationResult::createError([
'Drupal cannot be automatically updated from its current version, 9.7.1, to the recommended version, 9.8.1, because automatic updates from one minor version to another are not supported during cron.',
]);
return [
'cron disabled, minor updates not allowed' => [
FALSE,
CronUpdater::DISABLED,
[$update_disallowed],
],
'cron disabled, minor updates allowed' => [
TRUE,
CronUpdater::DISABLED,
[],
],
'security updates during cron, minor updates not allowed' => [
FALSE,
CronUpdater::SECURITY,
[$update_disallowed],
],
'security updates during cron, minor updates allowed' => [
TRUE,
CronUpdater::SECURITY,
[$cron_update_disallowed],
],
'cron enabled, minor updates not allowed' => [
FALSE,
CronUpdater::ALL,
[$update_disallowed],
],
'cron enabled, minor updates allowed' => [
TRUE,
CronUpdater::ALL,
[$cron_update_disallowed],
],
];
}
/**
* Tests an update version that is a different minor version than the current.
*
* @param bool $allow_minor_updates
* Whether or not updates across minor core versions are allowed in config.
* @param string $cron_setting
* Whether cron updates are enabled, and how often; should be one of the
* constants in \Drupal\automatic_updates\CronUpdater. This determines which
* stage the validator will use; if cron updates are enabled at all,
* it will be an instance of CronUpdater.
* @param \Drupal\package_manager\ValidationResult[] $expected_results
* The validation results that should be returned from by the validation
* manager, and logged if cron updates are enabled.
*
* @dataProvider providerMinorUpdates
*/
public function testMinorUpdates(): void {
public function testMinorUpdates(bool $allow_minor_updates, string $cron_setting, array $expected_results): void {
$this->config('automatic_updates.settings')
->set('allow_core_minor_updates', $allow_minor_updates)
->set('cron', $cron_setting)
->save();
// In order to test what happens when only security updates are enabled
// during cron (the default behavior), ensure that the latest available
// release is a security update.
$this->setReleaseMetadata(__DIR__ . '/../../../fixtures/release-history/drupal.9.8.1-security.xml');
$this->setCoreVersion('9.7.1');
$result = ValidationResult::createError([
'Drupal cannot be automatically updated from its current version, 9.7.1, to the recommended version, 9.8.1, because automatic updates from one minor version to another are not supported.',
]);
$this->assertCheckerResultsFromManager([$result], TRUE);
$this->assertCheckerResultsFromManager($expected_results, TRUE);
$logger = new TestLogger();
$this->container->get('logger.factory')
->get('automatic_updates')
->addLogger($logger);
$this->container->get('cron')->run();
// If cron updates are disabled, the update shouldn't have been started and
// nothing should have been logged.
if ($cron_setting === CronUpdater::DISABLED) {
$this->assertUpdateStagedTimes(0);
$this->assertEmpty($logger->records);
}
// If cron updates are enabled, the validation errors have been logged, and
// the update shouldn't have been started.
elseif ($expected_results) {
$this->assertUpdateStagedTimes(0);
// An exception exactly like this one should have been thrown by
// CronUpdater::dispatch(), and subsequently caught, formatted as HTML,
// and logged.
$exception = new StageValidationException($expected_results, 'Unable to complete the update because of errors.');
$log_message = TestCronUpdater::formatValidationException($exception);
$this->assertTrue($logger->hasRecord($log_message, RfcLogLevel::ERROR));
}
// If cron updates are enabled and no validation errors were expected, the
// update should have started and nothing should have been logged.
else {
$this->assertUpdateStagedTimes(1);
$this->assertEmpty($logger->records);
}
}
/**
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment