From de724027fe48a96562bf6566cd2f62de2ba64a73 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Wed, 20 Jun 2018 19:03:54 +0100 Subject: [PATCH] Issue #2788777 by alexpott, bircher, jribeiro, Eli-T, mpotter, douggreen, GoZ, DamienMcKenna, Dane Powell, jibran, szeidler, Alumei, andypost, dawehner, johndevman: Allow a site-specific profile to be installed from existing config --- core/includes/install.core.inc | 182 +++++++++++++++++- core/includes/install.inc | 28 ++- .../lib/Drupal/Core/Config/ConfigImporter.php | 8 + .../Config/Importer/ConfigImporterBatch.php | 77 ++++++++ .../Core/Installer/Form/SiteConfigureForm.php | 32 ++- core/modules/config/src/Form/ConfigSync.php | 50 ++--- .../ConfigUninstallViaCliImportTest.php | 1 + .../ContentModerationWorkflowConfigTest.php | 2 +- .../ContentTranslationConfigImportTest.php | 1 + .../Kernel/Config/NodeImportChangeTest.php | 2 +- .../Kernel/Config/NodeImportCreateTest.php | 2 +- .../system/src/SystemConfigSubscriber.php | 3 + core/modules/system/system.install | 13 ++ ...nstallerExistingConfigMultilingualTest.php | 24 +++ .../InstallerExistingConfigNoConfigTest.php | 42 ++++ ...nstallerExistingConfigNoSystemSiteTest.php | 49 +++++ ...tallerExistingConfigProfileHookInstall.php | 64 ++++++ .../Installer/InstallerExistingConfigTest.php | 30 +++ .../InstallerExistingConfigTestBase.php | 99 ++++++++++ .../Core/Config/ConfigImportRecreateTest.php | 2 +- .../ConfigImportRenameValidationTest.php | 2 +- .../ConfigImporterMissingContentTest.php | 2 +- .../Core/Config/ConfigImporterTest.php | 2 +- .../Core/Config/ConfigOverrideTest.php | 1 + .../Core/Config/ConfigSnapshotTest.php | 1 + .../Entity/ContentEntityNullStorageTest.php | 1 + .../config_install/multilingual.tar.gz | 52 +++++ .../testing_config_install.tar.gz | 43 +++++ .../testing_config_install_no_config.tar.gz | 1 + 29 files changed, 758 insertions(+), 58 deletions(-) create mode 100644 core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigMultilingualTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoConfigTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoSystemSiteTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigProfileHookInstall.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php create mode 100644 core/tests/fixtures/config_install/multilingual.tar.gz create mode 100644 core/tests/fixtures/config_install/testing_config_install.tar.gz create mode 100644 core/tests/fixtures/config_install/testing_config_install_no_config.tar.gz diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index c608052cfb6e..f2a5a6a64d73 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -6,6 +6,11 @@ */ use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Batch\BatchBuilder; +use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\ConfigImporterException; +use Drupal\Core\Config\Importer\ConfigImporterBatch; +use Drupal\Core\Config\StorageComparer; use Drupal\Core\DrupalKernel; use Drupal\Core\Database\Database; use Drupal\Core\Database\DatabaseExceptionWrapper; @@ -198,6 +203,10 @@ function install_state_defaults() { // The last task that was completed during the previous installation // request. 'completed_task' => NULL, + // Partial configuration cached during an installation from existing config. + 'config' => NULL, + // The path to the configuration to install when installing from config. + 'config_install_path' => NULL, // TRUE when there are valid config directories. 'config_verified' => FALSE, // TRUE when there is a valid database connection. @@ -473,9 +482,13 @@ function install_begin_request($class_loader, &$install_state) { // @todo Remove as part of https://www.drupal.org/node/2186491 drupal_get_filename('module', 'system', 'core/modules/system/system.info.yml'); - // Use the language from the profile configuration, if available, to override - // the language previously set in the parameters. - if (isset($install_state['profile_info']['distribution']['langcode'])) { + // Use the language from profile configuration if available. + if (!empty($install_state['config_install_path']) && $install_state['config']['system.site']) { + $install_state['parameters']['langcode'] = $install_state['config']['system.site']['default_langcode']; + } + elseif (isset($install_state['profile_info']['distribution']['langcode'])) { + // Otherwise, Use the language from the profile configuration, if available, + // to override the language previously set in the parameters. $install_state['parameters']['langcode'] = $install_state['profile_info']['distribution']['langcode']; } @@ -818,6 +831,30 @@ function install_tasks($install_state) { ], ]; + if (!empty($install_state['config_install_path'])) { + // The chosen profile indicates that rather than installing a new site, an + // instance of the same site should be installed from the given + // configuration. + // That means we need to remove the steps installing the extensions and + // replace them with a configuration synchronization step. + unset($tasks['install_download_translation']); + $key = array_search('install_profile_modules', array_keys($tasks), TRUE); + unset($tasks['install_profile_modules']); + unset($tasks['install_profile_themes']); + unset($tasks['install_install_profile']); + $config_tasks = [ + 'install_config_import_batch' => [ + 'display_name' => t('Install configuration'), + 'type' => 'batch', + ], + 'install_config_download_translations' => [], + 'install_config_revert_install_changes' => [], + ]; + $tasks = array_slice($tasks, 0, $key, TRUE) + + $config_tasks + + array_slice($tasks, $key, NULL, TRUE); + } + // Now add any tasks defined by the installation profile. if (!empty($install_state['parameters']['profile'])) { // Load the profile install file, because it is not always loaded when @@ -1494,6 +1531,14 @@ function install_load_profile(&$install_state) { $profile = $install_state['parameters']['profile']; $install_state['profiles'][$profile]->load(); $install_state['profile_info'] = install_profile_info($profile, isset($install_state['parameters']['langcode']) ? $install_state['parameters']['langcode'] : 'en'); + // If the profile has a config/sync directory copy the information to the + // install_state global. + if (!empty($install_state['profile_info']['config_install_path'])) { + $install_state['config_install_path'] = $install_state['profile_info']['config_install_path']; + if (!empty($install_state['profile_info']['config'])) { + $install_state['config'] = $install_state['profile_info']['config']; + } + } } /** @@ -2260,3 +2305,134 @@ function install_write_profile($install_state) { throw new InstallProfileMismatchException($install_state['parameters']['profile'], $settings_profile, $settings_path, \Drupal::translation()); } } + +/** + * Creates a batch for the config importer to process. + * + * @see install_tasks() + */ +function install_config_import_batch() { + // We need to manually trigger the installation of core-provided entity types, + // as those will not be handled by the module installer. + // @see install_profile_modules() + install_core_entity_type_definitions(); + + // Get the sync storage. + $sync = \Drupal::service('config.storage.sync'); + // Match up the site UUIDs, the install_base_system install task will have + // installed the system module and created a new UUID. + $system_site = $sync->read('system.site'); + \Drupal::configFactory()->getEditable('system.site')->set('uuid', $system_site['uuid'])->save(); + + // Create the storage comparer and the config importer. + $config_manager = \Drupal::service('config.manager'); + $storage_comparer = new StorageComparer($sync, \Drupal::service('config.storage'), $config_manager); + $storage_comparer->createChangelist(); + $config_importer = new ConfigImporter( + $storage_comparer, + \Drupal::service('event_dispatcher'), + $config_manager, + \Drupal::service('lock.persistent'), + \Drupal::service('config.typed'), + \Drupal::service('module_handler'), + \Drupal::service('module_installer'), + \Drupal::service('theme_handler'), + \Drupal::service('string_translation') + ); + + try { + $sync_steps = $config_importer->initialize(); + + $batch_builder = new BatchBuilder(); + $batch_builder + ->setFinishCallback([ConfigImporterBatch::class, 'finish']) + ->setTitle(t('Importing configuration')) + ->setInitMessage(t('Starting configuration import.')) + ->setErrorMessage(t('Configuration import has encountered an error.')); + + foreach ($sync_steps as $sync_step) { + $batch_builder->addOperation([ConfigImporterBatch::class, 'process'], [$config_importer, $sync_step]); + } + + return $batch_builder->toArray(); + } + catch (ConfigImporterException $e) { + global $install_state; + // There are validation errors. + $messenger = \Drupal::messenger(); + $messenger->addError(t('The configuration synchronization failed validation.')); + foreach ($config_importer->getErrors() as $message) { + $messenger->addError($message); + } + install_display_output(['#title' => t('Configuration validation')], $install_state); + } +} + +/** + * Replaces install_download_translation() during configuration installs. + * + * @param array $install_state + * An array of information about the current installation state. + * + * @return string + * A themed status report, or an exception if there are requirement errors. + * Upon successful download the page is reloaded and no output is returned. + * + * @see install_download_translation() + */ +function install_config_download_translations(&$install_state) { + $needs_download = isset($install_state['parameters']['langcode']) && !isset($install_state['translations'][$install_state['parameters']['langcode']]) && $install_state['parameters']['langcode'] !== 'en'; + if ($needs_download) { + return install_download_translation($install_state); + } +} + +/** + * Reverts configuration if hook_install() implementations have made changes. + * + * This step ensures that the final configuration matches the configuration + * provided to the installer. + */ +function install_config_revert_install_changes() { + global $install_state; + + $config_manager = \Drupal::service('config.manager'); + $storage_comparer = new StorageComparer(\Drupal::service('config.storage.sync'), \Drupal::service('config.storage'), $config_manager); + $storage_comparer->createChangelist(); + if ($storage_comparer->hasChanges()) { + $config_importer = new ConfigImporter( + $storage_comparer, + \Drupal::service('event_dispatcher'), + $config_manager, + \Drupal::service('lock.persistent'), + \Drupal::service('config.typed'), + \Drupal::service('module_handler'), + \Drupal::service('module_installer'), + \Drupal::service('theme_handler'), + \Drupal::service('string_translation') + ); + try { + $config_importer->import(); + } + catch (ConfigImporterException $e) { + global $install_state; + $messenger = \Drupal::messenger(); + // There are validation errors. + $messenger->addError(t('The configuration synchronization failed validation.')); + foreach ($config_importer->getErrors() as $message) { + $messenger->addError($message); + } + install_display_output(['#title' => t('Configuration validation')], $install_state); + } + + // At this point the configuration should match completely. + if (\Drupal::moduleHandler()->moduleExists('language')) { + // If the English language exists at this point we need to ensure + // install_download_additional_translations_operations() does not delete + // it. + if (ConfigurableLanguage::load('en')) { + $install_state['profile_info']['keep_english'] = TRUE; + } + } + } +} diff --git a/core/includes/install.inc b/core/includes/install.inc index 56b450ec95c1..d65c85fff971 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -10,6 +10,7 @@ use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\OpCodeCache; use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Config\FileStorage; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Site\Settings; @@ -481,12 +482,20 @@ function _drupal_rewrite_settings_dump_one(\stdClass $variable, $prefix = '', $s * @see update_prepare_d8_bootstrap() */ function drupal_install_config_directories() { - global $config_directories; + global $config_directories, $install_state; - // Add a randomized config directory name to settings.php, unless it was - // manually defined in the existing already. + // If settings.php does not contain a config sync directory name we need to + // configure one. if (empty($config_directories[CONFIG_SYNC_DIRECTORY])) { - $config_directories[CONFIG_SYNC_DIRECTORY] = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync'; + if (empty($install_state['config_install_path'])) { + // Add a randomized config directory name to settings.php + $config_directories[CONFIG_SYNC_DIRECTORY] = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync'; + } + else { + // Install profiles can contain a config sync directory. If they do, + // 'config_install_path' is a path to the directory. + $config_directories[CONFIG_SYNC_DIRECTORY] = $install_state['config_install_path']; + } $settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [ 'value' => $config_directories[CONFIG_SYNC_DIRECTORY], 'required' => TRUE, @@ -1099,9 +1108,10 @@ function install_profile_info($profile, $langcode = 'en') { 'version' => NULL, 'hidden' => FALSE, 'php' => DRUPAL_MINIMUM_PHP, + 'config_install_path' => NULL, ]; - $profile_file = drupal_get_path('profile', $profile) . "/$profile.info.yml"; - $info = \Drupal::service('info_parser')->parse($profile_file); + $profile_path = drupal_get_path('profile', $profile); + $info = \Drupal::service('info_parser')->parse("$profile_path/$profile.info.yml"); $info += $defaults; // drupal_required_modules() includes the current profile as a dependency. @@ -1114,6 +1124,12 @@ function install_profile_info($profile, $langcode = 'en') { // remove any duplicates. $info['install'] = array_unique(array_merge($info['install'], $required, $info['dependencies'], $locale)); + // If the profile has a config/sync directory use that to install drupal. + if (is_dir($profile_path . '/config/sync')) { + $info['config_install_path'] = $profile_path . '/config/sync'; + $sync = new FileStorage($profile_path . '/config/sync'); + $info['config']['system.site'] = $sync->read('system.site'); + } $cache[$profile][$langcode] = $info; } return $cache[$profile][$langcode]; diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index b58f96358ba7..c09372775068 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -405,6 +405,14 @@ protected function createExtensionChangelist() { $module_list = array_reverse($module_list); $this->extensionChangelist['module']['install'] = array_intersect(array_keys($module_list), $install); + // If we're installing the install profile ensure it comes last. This will + // occur when installing a site from configuration. + $install_profile_key = array_search($new_extensions['profile'], $this->extensionChangelist['module']['install'], TRUE); + if ($install_profile_key !== FALSE) { + unset($this->extensionChangelist['module']['install'][$install_profile_key]); + $this->extensionChangelist['module']['install'][] = $new_extensions['profile']; + } + // Work out what themes to install and to uninstall. $this->extensionChangelist['theme']['install'] = array_keys(array_diff_key($new_extensions['theme'], $current_extensions['theme'])); $this->extensionChangelist['theme']['uninstall'] = array_keys(array_diff_key($current_extensions['theme'], $new_extensions['theme'])); diff --git a/core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php b/core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php new file mode 100644 index 000000000000..8aee289e0d14 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php @@ -0,0 +1,77 @@ +<?php + +namespace Drupal\Core\Config\Importer; + +use Drupal\Core\Config\ConfigImporter; + +/** + * Methods for running the ConfigImporter in a batch. + * + * @see \Drupal\Core\Config\ConfigImporter + */ +class ConfigImporterBatch { + + /** + * Processes the config import batch and persists the importer. + * + * @param \Drupal\Core\Config\ConfigImporter $config_importer + * The batch config importer object to persist. + * @param string $sync_step + * The synchronization step to do. + * @param array $context + * The batch context. + */ + public static function process(ConfigImporter $config_importer, $sync_step, &$context) { + if (!isset($context['sandbox']['config_importer'])) { + $context['sandbox']['config_importer'] = $config_importer; + } + + $config_importer = $context['sandbox']['config_importer']; + $config_importer->doSyncStep($sync_step, $context); + if ($errors = $config_importer->getErrors()) { + if (!isset($context['results']['errors'])) { + $context['results']['errors'] = []; + } + $context['results']['errors'] = array_merge($errors, $context['results']['errors']); + } + } + + /** + * Finish batch. + * + * This function is a static function to avoid serializing the ConfigSync + * object unnecessarily. + * + * @param bool $success + * Indicate that the batch API tasks were all completed successfully. + * @param array $results + * An array of all the results that were updated in update_do_one(). + * @param array $operations + * A list of the operations that had not been completed by the batch API. + */ + public static function finish($success, $results, $operations) { + $messenger = \Drupal::messenger(); + if ($success) { + if (!empty($results['errors'])) { + $logger = \Drupal::logger('config_sync'); + foreach ($results['errors'] as $error) { + $messenger->addError($error); + $logger->error($error); + } + $messenger->addWarning(t('The configuration was imported with errors.')); + } + elseif (!drupal_installation_attempted()) { + // Display a success message when not installing Drupal. + $messenger->addStatus(t('The configuration was imported successfully.')); + } + } + else { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $message = t('An error occurred while processing %error_operation with arguments: @arguments', ['%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)]); + $messenger->addError($message); + } + } + +} diff --git a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php index d5f8f56a791d..2cca96259993 100644 --- a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php +++ b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php @@ -121,6 +121,7 @@ protected function getEditableConfigNames() { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { + global $install_state; $form['#title'] = $this->t('Configure site'); // Warn about settings.php permissions risk @@ -148,12 +149,14 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['site_information'] = [ '#type' => 'fieldgroup', '#title' => $this->t('Site information'), + '#access' => empty($install_state['config_install_path']), ]; $form['site_information']['site_name'] = [ '#type' => 'textfield', '#title' => $this->t('Site name'), '#required' => TRUE, '#weight' => -20, + '#access' => empty($install_state['config_install_path']), ]; $form['site_information']['site_mail'] = [ '#type' => 'email', @@ -162,6 +165,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#description' => $this->t("Automated emails, such as registration information, will be sent from this address. Use an address ending in your site's domain to help prevent these emails from being flagged as spam."), '#required' => TRUE, '#weight' => -15, + '#access' => empty($install_state['config_install_path']), ]; $form['admin_account'] = [ @@ -191,6 +195,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['regional_settings'] = [ '#type' => 'fieldgroup', '#title' => $this->t('Regional settings'), + '#access' => empty($install_state['config_install_path']), ]; $countries = $this->countryManager->getList(); $form['regional_settings']['site_default_country'] = [ @@ -201,6 +206,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#options' => $countries, '#description' => $this->t('Select the default country for the site.'), '#weight' => 0, + '#access' => empty($install_state['config_install_path']), ]; $form['regional_settings']['date_default_timezone'] = [ '#type' => 'select', @@ -211,17 +217,20 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#description' => $this->t('By default, dates in this site will be displayed in the chosen time zone.'), '#weight' => 5, '#attributes' => ['class' => ['timezone-detect']], + '#access' => empty($install_state['config_install_path']), ]; $form['update_notifications'] = [ '#type' => 'fieldgroup', '#title' => $this->t('Update notifications'), '#description' => $this->t('The system will notify you when updates and important security releases are available for installed components. Anonymous information about your site is sent to <a href=":drupal">Drupal.org</a>.', [':drupal' => 'https://www.drupal.org']), + '#access' => empty($install_state['config_install_path']), ]; $form['update_notifications']['enable_update_status_module'] = [ '#type' => 'checkbox', '#title' => $this->t('Check for updates automatically'), '#default_value' => 1, + '#access' => empty($install_state['config_install_path']), ]; $form['update_notifications']['enable_update_status_emails'] = [ '#type' => 'checkbox', @@ -232,6 +241,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { 'input[name="enable_update_status_module"]' => ['checked' => TRUE], ], ], + '#access' => empty($install_state['config_install_path']), ]; $form['actions'] = ['#type' => 'actions']; @@ -258,21 +268,25 @@ public function validateForm(array &$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - $this->config('system.site') - ->set('name', (string) $form_state->getValue('site_name')) - ->set('mail', (string) $form_state->getValue('site_mail')) - ->save(TRUE); + global $install_state; - $this->config('system.date') - ->set('timezone.default', (string) $form_state->getValue('date_default_timezone')) - ->set('country.default', (string) $form_state->getValue('site_default_country')) - ->save(TRUE); + if (empty($install_state['config_install_path'])) { + $this->config('system.site') + ->set('name', (string) $form_state->getValue('site_name')) + ->set('mail', (string) $form_state->getValue('site_mail')) + ->save(TRUE); + + $this->config('system.date') + ->set('timezone.default', (string) $form_state->getValue('date_default_timezone')) + ->set('country.default', (string) $form_state->getValue('site_default_country')) + ->save(TRUE); + } $account_values = $form_state->getValue('account'); // Enable update.module if this option was selected. $update_status_module = $form_state->getValue('enable_update_status_module'); - if ($update_status_module) { + if (empty($install_state['config_install_path']) && $update_status_module) { $this->moduleInstaller->install(['file', 'update'], FALSE); // Add the site maintenance account's email address to the list of diff --git a/core/modules/config/src/Form/ConfigSync.php b/core/modules/config/src/Form/ConfigSync.php index 73c7a1b8b1ad..57fb1d860e00 100644 --- a/core/modules/config/src/Form/ConfigSync.php +++ b/core/modules/config/src/Form/ConfigSync.php @@ -4,6 +4,7 @@ use Drupal\Core\Config\ConfigImporterException; use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\Importer\ConfigImporterBatch; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleInstallerInterface; @@ -337,14 +338,14 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $sync_steps = $config_importer->initialize(); $batch = [ 'operations' => [], - 'finished' => [get_class($this), 'finishBatch'], + 'finished' => [ConfigImporterBatch::class, 'finish'], 'title' => t('Synchronizing configuration'), 'init_message' => t('Starting configuration synchronization.'), 'progress_message' => t('Completed step @current of @total.'), 'error_message' => t('Configuration synchronization has encountered an error.'), ]; foreach ($sync_steps as $sync_step) { - $batch['operations'][] = [[get_class($this), 'processBatch'], [$config_importer, $sync_step]]; + $batch['operations'][] = [[ConfigImporterBatch::class, 'process'], [$config_importer, $sync_step]]; } batch_set($batch); @@ -368,20 +369,15 @@ public function submitForm(array &$form, FormStateInterface $form_state) { * The synchronization step to do. * @param array $context * The batch context. + * + * @deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use + * \Drupal\Core\Config\Importer\ConfigImporterBatch::process() instead. + * + * @see https://www.drupal.org/node/2897299 */ public static function processBatch(ConfigImporter $config_importer, $sync_step, &$context) { - if (!isset($context['sandbox']['config_importer'])) { - $context['sandbox']['config_importer'] = $config_importer; - } - - $config_importer = $context['sandbox']['config_importer']; - $config_importer->doSyncStep($sync_step, $context); - if ($errors = $config_importer->getErrors()) { - if (!isset($context['results']['errors'])) { - $context['results']['errors'] = []; - } - $context['results']['errors'] = array_merge($context['results']['errors'], $errors); - } + @trigger_error('\Drupal\config\Form\ConfigSync::processBatch() deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use \Drupal\Core\Config\Importer\ConfigImporterBatch::process() instead. See https://www.drupal.org/node/2897299'); + ConfigImporterBatch::process($config_importer, $sync_step, $context); } /** @@ -389,27 +385,15 @@ public static function processBatch(ConfigImporter $config_importer, $sync_step, * * This function is a static function to avoid serializing the ConfigSync * object unnecessarily. + * + * @deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use + * \Drupal\Core\Config\Importer\ConfigImporterBatch::finish() instead. + * + * @see https://www.drupal.org/node/2897299 */ public static function finishBatch($success, $results, $operations) { - if ($success) { - if (!empty($results['errors'])) { - foreach ($results['errors'] as $error) { - \Drupal::messenger()->addError($error); - \Drupal::logger('config_sync')->error($error); - } - \Drupal::messenger()->addWarning(\Drupal::translation()->translate('The configuration was imported with errors.')); - } - else { - \Drupal::messenger()->addStatus(\Drupal::translation()->translate('The configuration was imported successfully.')); - } - } - else { - // An error occurred. - // $operations contains the operations that remained unprocessed. - $error_operation = reset($operations); - $message = \Drupal::translation()->translate('An error occurred while processing %error_operation with arguments: @arguments', ['%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)]); - \Drupal::messenger()->addError($message); - } + @trigger_error('\Drupal\config\Form\ConfigSync::finishBatch() deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use \Drupal\Core\Config\Importer\ConfigImporterBatch::finish() instead. See https://www.drupal.org/node/2897299'); + ConfigImporterBatch::finish($success, $results, $operations); } } diff --git a/core/modules/config/tests/src/Kernel/ConfigUninstallViaCliImportTest.php b/core/modules/config/tests/src/Kernel/ConfigUninstallViaCliImportTest.php index 5f77447f316f..0ab0fd1764e8 100644 --- a/core/modules/config/tests/src/Kernel/ConfigUninstallViaCliImportTest.php +++ b/core/modules/config/tests/src/Kernel/ConfigUninstallViaCliImportTest.php @@ -32,6 +32,7 @@ protected function setUp() { $this->markTestSkipped('This test has to be run from the CLI'); } + $this->installConfig(['system']); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); // Set up the ConfigImporter object for testing. diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php index ee4392738bf3..1b2125b7dd53 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php @@ -59,7 +59,7 @@ protected function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('user'); $this->installEntitySchema('content_moderation_state'); - $this->installConfig('content_moderation'); + $this->installConfig(['system', 'content_moderation']); NodeType::create([ 'type' => 'example', diff --git a/core/modules/content_translation/tests/src/Kernel/ContentTranslationConfigImportTest.php b/core/modules/content_translation/tests/src/Kernel/ContentTranslationConfigImportTest.php index 44e816089ec1..0fe6b9f6e0b3 100644 --- a/core/modules/content_translation/tests/src/Kernel/ContentTranslationConfigImportTest.php +++ b/core/modules/content_translation/tests/src/Kernel/ContentTranslationConfigImportTest.php @@ -33,6 +33,7 @@ class ContentTranslationConfigImportTest extends KernelTestBase { protected function setUp() { parent::setUp(); + $this->installConfig(['system']); $this->installEntitySchema('entity_test_mul'); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); diff --git a/core/modules/node/tests/src/Kernel/Config/NodeImportChangeTest.php b/core/modules/node/tests/src/Kernel/Config/NodeImportChangeTest.php index e1b17f2c1c9a..d670e6a44cae 100644 --- a/core/modules/node/tests/src/Kernel/Config/NodeImportChangeTest.php +++ b/core/modules/node/tests/src/Kernel/Config/NodeImportChangeTest.php @@ -26,7 +26,7 @@ protected function setUp() { parent::setUp(); // Set default storage backend. - $this->installConfig(['field', 'node_test_config']); + $this->installConfig(['system', 'field', 'node_test_config']); } /** diff --git a/core/modules/node/tests/src/Kernel/Config/NodeImportCreateTest.php b/core/modules/node/tests/src/Kernel/Config/NodeImportCreateTest.php index cb985173ae96..0b27bc2a22c7 100644 --- a/core/modules/node/tests/src/Kernel/Config/NodeImportCreateTest.php +++ b/core/modules/node/tests/src/Kernel/Config/NodeImportCreateTest.php @@ -28,7 +28,7 @@ protected function setUp() { $this->installEntitySchema('user'); // Set default storage backend. - $this->installConfig(['field']); + $this->installConfig(['system', 'field']); } /** diff --git a/core/modules/system/src/SystemConfigSubscriber.php b/core/modules/system/src/SystemConfigSubscriber.php index 519155b8497c..0ab0d15fc610 100644 --- a/core/modules/system/src/SystemConfigSubscriber.php +++ b/core/modules/system/src/SystemConfigSubscriber.php @@ -72,6 +72,9 @@ public function onConfigImporterValidateNotEmpty(ConfigImporterEvent $event) { * The config import event. */ public function onConfigImporterValidateSiteUUID(ConfigImporterEvent $event) { + if (!$event->getConfigImporter()->getStorageComparer()->getSourceStorage()->exists('system.site')) { + $event->getConfigImporter()->logError($this->t('This import does not contain system.site configuration, so has been rejected.')); + } if (!$event->getConfigImporter()->getStorageComparer()->validateSiteUuid()) { $event->getConfigImporter()->logError($this->t('Site UUID in source storage does not match the target storage.')); } diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 3ad360ff242e..0fe129b0b6d0 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1002,6 +1002,19 @@ function system_requirements($phase) { ]; } + // During installs from configuration don't support install profiles that + // implement hook_install. + if ($phase == 'install' && !empty($install_state['config_install_path'])) { + $install_hook = $install_state['parameters']['profile'] . '_install'; + if (function_exists($install_hook)) { + $requirements['config_install'] = [ + 'title' => t('Configuration install'), + 'value' => $install_state['parameters']['profile'], + 'description' => t('The selected profile has a hook_install() implementation and therefore can not be installed from configuration.'), + 'severity' => REQUIREMENT_ERROR, + ]; + } + } return $requirements; } diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigMultilingualTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigMultilingualTest.php new file mode 100644 index 000000000000..db05d32031e6 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigMultilingualTest.php @@ -0,0 +1,24 @@ +<?php + +namespace Drupal\FunctionalTests\Installer; + +/** + * Verifies that installing from existing configuration works. + * + * @group Installer + */ +class InstallerExistingConfigMultilingualTest extends InstallerExistingConfigTestBase { + + /** + * {@inheritdoc} + */ + protected $profile = 'testing_config_install_multilingual'; + + /** + * {@inheritdoc} + */ + protected function getConfigTarball() { + return __DIR__ . '/../../../fixtures/config_install/multilingual.tar.gz'; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoConfigTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoConfigTest.php new file mode 100644 index 000000000000..01ddefa9048a --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoConfigTest.php @@ -0,0 +1,42 @@ +<?php + +namespace Drupal\FunctionalTests\Installer; + +/** + * Verifies that profiles invalid config can not be installed. + * + * @group Installer + */ +class InstallerExistingConfigNoConfigTest extends InstallerExistingConfigTestBase { + + protected $profile = 'no_config_profile'; + + /** + * Final installer step: Configure site. + */ + protected function setUpSite() { + // There are errors therefore there is nothing to do here. + return; + } + + /** + * {@inheritdoc} + */ + protected function getConfigTarball() { + return __DIR__ . '/../../../fixtures/config_install/testing_config_install_no_config.tar.gz'; + } + + /** + * Tests that profiles with an empty config/sync directory do not work. + */ + public function testConfigSync() { + $this->assertTitle('Configuration validation | Drupal'); + $this->assertText('The configuration synchronization failed validation.'); + $this->assertText('This import is empty and if applied would delete all of your configuration, so has been rejected.'); + + // Ensure there is no continuation button. + $this->assertNoText('Save and continue'); + $this->assertNoFieldById('edit-submit'); + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoSystemSiteTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoSystemSiteTest.php new file mode 100644 index 000000000000..4ac68d4e8637 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoSystemSiteTest.php @@ -0,0 +1,49 @@ +<?php + +namespace Drupal\FunctionalTests\Installer; + +/** + * Testing installing from config without system.site. + * + * @group Installer + */ +class InstallerExistingConfigNoSystemSiteTest extends InstallerExistingConfigTestBase { + + /** + * {@inheritdoc} + */ + protected function prepareEnvironment() { + parent::prepareEnvironment(); + // File API functions are not available yet. + unlink($this->siteDirectory . '/profiles/' . $this->profile . '/config/sync/system.site.yml'); + } + + /** + * {@inheritdoc} + */ + public function setUpSite() { + return; + } + + /** + * Tests that profiles with no system.site do not work. + */ + public function testConfigSync() { + $this->htmlOutput(NULL); + $this->assertTitle('Configuration validation | Drupal'); + $this->assertText('The configuration synchronization failed validation.'); + $this->assertText('This import does not contain system.site configuration, so has been rejected.'); + + // Ensure there is no continuation button. + $this->assertNoText('Save and continue'); + $this->assertNoFieldById('edit-submit'); + } + + /** + * {@inheritdoc} + */ + protected function getConfigTarball() { + return __DIR__ . '/../../../fixtures/config_install/testing_config_install.tar.gz'; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigProfileHookInstall.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigProfileHookInstall.php new file mode 100644 index 000000000000..2d9d6cdfc121 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigProfileHookInstall.php @@ -0,0 +1,64 @@ +<?php + +namespace Drupal\FunctionalTests\Installer; + +/** + * Verifies that profiles with hook_install() can't be installed from config. + * + * @group Installer + */ +class InstallerExistingConfigProfileHookInstall extends InstallerExistingConfigTestBase { + + protected $profile = 'config_profile_with_hook_install'; + + /** + * {@inheritdoc} + */ + protected function visitInstaller() { + // Create an .install file with a hook_install() implementation. + $path = $this->siteDirectory . '/profiles/' . $this->profile; + $contents = <<<EOF +<?php + +function config_profile_with_hook_install_install() { +} +EOF; + file_put_contents("$path/{$this->profile}.install", $contents); + parent::visitInstaller(); + } + + /** + * Installer step: Configure settings. + */ + protected function setUpSettings() { + // There are errors therefore there is nothing to do here. + return; + } + + /** + * Final installer step: Configure site. + */ + protected function setUpSite() { + // There are errors therefore there is nothing to do here. + return; + } + + /** + * {@inheritdoc} + */ + protected function getConfigTarball() { + // We're not going to get to the config import stage so this does not + // matter. + return __DIR__ . '/../../../fixtures/config_install/testing_config_install_no_config.tar.gz'; + } + + /** + * Confirms the installation has failed and the expected error is displayed. + */ + public function testConfigSync() { + $this->assertTitle('Requirements problem | Drupal'); + $this->assertText($this->profile); + $this->assertText('The selected profile has a hook_install() implementation and therefore can not be installed from configuration.'); + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTest.php new file mode 100644 index 000000000000..ebf4c2a8c5b4 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTest.php @@ -0,0 +1,30 @@ +<?php + +namespace Drupal\FunctionalTests\Installer; + +/** + * Verifies that installing from existing configuration works. + * + * @group Installer + */ +class InstallerExistingConfigTest extends InstallerExistingConfigTestBase { + + /** + * {@inheritdoc} + */ + public function setUpSite() { + // The configuration is from a site installed in French. + // So after selecting the profile the installer detects that the site must + // be installed in French, thus we change the button translation. + $this->translations['Save and continue'] = 'Enregistrer et continuer'; + parent::setUpSite(); + } + + /** + * {@inheritdoc} + */ + protected function getConfigTarball() { + return __DIR__ . '/../../../fixtures/config_install/testing_config_install.tar.gz'; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php new file mode 100644 index 000000000000..e093be966769 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php @@ -0,0 +1,99 @@ +<?php + +namespace Drupal\FunctionalTests\Installer; + +use Drupal\Component\Serialization\Yaml; +use Drupal\Core\Archiver\ArchiveTar; + +/** + * Provides a base class for testing installing from existing configuration. + */ +abstract class InstallerExistingConfigTestBase extends InstallerTestBase { + + /** + * This is set by the profile in the core.extension extracted. + */ + protected $profile = NULL; + + /** + * {@inheritdoc} + */ + protected function prepareEnvironment() { + parent::prepareEnvironment(); + $archiver = new ArchiveTar($this->getConfigTarball(), 'gz'); + + if ($this->profile === NULL) { + $core_extension = Yaml::decode($archiver->extractInString('core.extension.yml')); + $this->profile = $core_extension['profile']; + } + + // Create a profile for testing. + $info = [ + 'type' => 'profile', + 'core' => \Drupal::CORE_COMPATIBILITY, + 'name' => 'Configuration installation test profile (' . $this->profile . ')', + ]; + // File API functions are not available yet. + $path = $this->siteDirectory . '/profiles/' . $this->profile; + + mkdir($path, 0777, TRUE); + file_put_contents("$path/{$this->profile}.info.yml", Yaml::encode($info)); + + // Create config/sync directory and extract tarball contents to it. + $config_sync_directory = $path . '/config/sync'; + mkdir($config_sync_directory, 0777, TRUE); + $files = []; + $list = $archiver->listContent(); + if (is_array($list)) { + /** @var array $list */ + foreach ($list as $file) { + $files[] = $file['filename']; + } + $archiver->extractList($files, $config_sync_directory); + } + } + + /** + * Gets the filepath to the configuration tarball. + * + * The tarball will be extracted to the install profile's config/sync + * directory for testing. + * + * @return string + * The filepath to the configuration tarball. + */ + abstract protected function getConfigTarball(); + + /** + * {@inheritdoc} + */ + protected function installParameters() { + $parameters = parent::installParameters(); + + // The options that change configuration are disabled when installing from + // existing configuration. + unset($parameters['forms']['install_configure_form']['site_name']); + unset($parameters['forms']['install_configure_form']['site_mail']); + unset($parameters['forms']['install_configure_form']['update_status_module']); + + return $parameters; + } + + /** + * Confirms that the installation installed the configuration correctly. + */ + public function testConfigSync() { + // After installation there is no snapshot and nothing to import. + $change_list = $this->configImporter()->getStorageComparer()->getChangelist(); + $expected = [ + 'create' => [], + // The system.mail is changed configuration because the test system + // changes it to ensure that mails are not sent. + 'update' => ['system.mail'], + 'delete' => [], + 'rename' => [], + ]; + $this->assertEqual($expected, $change_list); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRecreateTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRecreateTest.php index 7d9c9ae23f76..fef464ed7f21 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRecreateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRecreateTest.php @@ -32,7 +32,7 @@ protected function setUp() { parent::setUp(); $this->installEntitySchema('node'); - $this->installConfig(['field', 'node']); + $this->installConfig(['system', 'field', 'node']); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRenameValidationTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRenameValidationTest.php index 84b5a2de1314..f52b3956ab96 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRenameValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRenameValidationTest.php @@ -39,7 +39,7 @@ protected function setUp() { $this->installEntitySchema('user'); $this->installEntitySchema('node'); - $this->installConfig(['field']); + $this->installConfig(['system', 'field']); // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer( diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php index 59f9cb2ed728..db09e3f9f66d 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php @@ -33,7 +33,7 @@ protected function setUp() { $this->installSchema('system', 'sequences'); $this->installEntitySchema('entity_test'); $this->installEntitySchema('user'); - $this->installConfig(['config_test']); + $this->installConfig(['system', 'config_test']); // Installing config_test's default configuration pollutes the global // variable being used for recording hook invocations by this test already, // so it has to be cleared out manually. diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php index dc4e0ae12b9d..6e8adf79a772 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php @@ -33,7 +33,7 @@ class ConfigImporterTest extends KernelTestBase { protected function setUp() { parent::setUp(); - $this->installConfig(['config_test']); + $this->installConfig(['system', 'config_test']); // Installing config_test's default configuration pollutes the global // variable being used for recording hook invocations by this test already, // so it has to be cleared out manually. diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigOverrideTest.php index b2ddaa396ddd..550e0d5b2b0f 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigOverrideTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigOverrideTest.php @@ -20,6 +20,7 @@ class ConfigOverrideTest extends KernelTestBase { protected function setUp() { parent::setUp(); + $this->installConfig(['system']); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); } diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSnapshotTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSnapshotTest.php index 62eff04d4178..645638d83239 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSnapshotTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSnapshotTest.php @@ -24,6 +24,7 @@ class ConfigSnapshotTest extends KernelTestBase { */ protected function setUp() { parent::setUp(); + $this->installConfig(['system']); // Update the config snapshot. This allows the parent::setUp() to write // configuration files. \Drupal::service('config.manager')->createSnapshot(\Drupal::service('config.storage'), \Drupal::service('config.storage.snapshot')); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityNullStorageTest.php b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityNullStorageTest.php index 6da9bb3baad0..001d9ba3dee8 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityNullStorageTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityNullStorageTest.php @@ -43,6 +43,7 @@ public function testEntityQuery() { * @see \Drupal\Core\Entity\Event\BundleConfigImportValidate */ public function testDeleteThroughImport() { + $this->installConfig(['system']); $contact_form = ContactForm::create(['id' => 'test']); $contact_form->save(); diff --git a/core/tests/fixtures/config_install/multilingual.tar.gz b/core/tests/fixtures/config_install/multilingual.tar.gz new file mode 100644 index 000000000000..d43aafa1e58a --- /dev/null +++ b/core/tests/fixtures/config_install/multilingual.tar.gz @@ -0,0 +1,52 @@ +‹�",æX�í}érã8¶fýöSp:¦ÃÝ›û’QÑ·µXò&ÉÚ,Ë·n(@”(q3m“ó0÷æÇÄ<B¿Ø�$%k±-g–LWgòˈ´B Eâ;888§uQ(×.ÎÂyøˇ¦i‘ç)òW…ø/Í&Ç1xŽ£Ž–ž¥EN h†æŠþ¸[zF„ÐÇ·-4«®foœO~ +µþûo‚ÎÈ(Ýô‘ºþ‚Ò\'„¦†9Œ|š®C….¥"Ê´=בN™.X¸‘O•ýȃ˜!:£:.eà ¢BÒævPÍ)ú•šš¸*uÛtΓç:š"Ëõlä„ç[_:ŽvFU\_Ðp};mJu£Ò‘g¹Óî\HEá!‡ +?E~ð+þ€¨QzÁ—óóÙlv¦Çw|æúøÊ®‘Ë&W‹oÊB䧷ñÙ¯&¨–«MÎ’ÿ &ƒø9œ-lëh×8ÄIwø/r"Ÿó?D‘©¡8F@†"+@Peð¢¡hè: qÐФ¢:CÍÕÑ +9'øq„Qð… +ý`2"GGŽf¢àË •R’|¢(@L*û3-:‹;.·]=²Ðv |Žý\JºãÉ@sý¸HGŒ¬p4>Á`ô…*÷ +EéŒÂ5òö¢#Ù >ÍM-RúN»¹÷Ê>�u^;!?t£‹Ÿ$K/ã£!øÈÔ‘ +ýaúAx2Cæp~¡˜Ïw§ø”ÿ…r"Ë:ñ¬hhâê§ÉÈÄ,ú7~z 0Äâ)~äʯפ(ªÈúBR ”ˆ2\þ|ÑõŠ«t3ð,¸øËSÕBä£äNÉ“òBülè“ø¬i™!®ù¿(êŸì¿ÿ}þ«>tt|çLJù¿;þK´ äüÏ ÿ5^1XA�§ó4à Ce5Àòê‚Èj‚ñ ü?»ÖÍC½¨0ÝyyzQ›,K-ù±À™Q+OªØaèªÈ䆿ÁîU~™à#1£Ö̦_cvJ×Uc e÷Xýr5£OÛX1¢VgOßEéSšÔ‹4 ZÕÀr‡nú˜7JH~Ùni€+C'-ùy‰ÿø#´Dasà(BàÿEŽÝæ?‹+J9ÿ³@™gò@Ð8ð2Ô€¢!hšŽD‰SuIßÀÿo&úaØMP(¶—BX÷ìû %ÕQ9eçÂœN]ϹvŠcK¾©_l}«§¾ÌvbÏ` MwÀ¼JøÖ^¡ûKužÉ~ç›6ÄFT<®SimÒ“§ð2åßG×£ã5þ‡0˜‡ýïà?¿7þ¢˜ó?$ügYž“9Ã�PBÿuȼ*8š§UhìGò_´¨ªV«/7öКH¼ç ;=…»ypq]YÔÍîcM•¯û{ü{ê;ÙÏ`ÜÖ›Ü߬±f~ªÁ;iNjÅRb=zߦþ\’ x‰ÿÃŒílíïÚÿ<Ïÿe‚„ÿ’¦Ñ¢G}Dóà9Kžf®²*ghãoáÿ¶þÕSÿ{„ß´ÚN¯¨Íç7µ«J«&j4¨ÛÍãêãêй÷5`-œæåÕ–P~›mÿªH ÷4÷ŠDØ«ð¬ +tñ)*>µ=þ§Ïâ+þt€}þÛ(àkðÿåüß›ÿ“šÍùŸRþC)²®‘tÀK,dB¬H4¯pøupÂ'ØÿB®O8ëÝÌóy±£-ÙÔoù‡‹.s)´‡—–\úz%(lH€U~ÅþǬ·ó‘þîI€U‹oOìÔÚ˜ˆŸµ:ÿþi€äÁ>ÿ=|ƒÐ-t, pÿôÿyšËùŸþÓ:+òŒ,AÖ±þ¯1(ªÂ2¬@«HÒ¿…ÿßLtyá…eCìùs´@Óî’wî„©áKÅpÞ|*kŽéŒîÙ«kf¸AôçžúNõŸ{•èÏM½Âñ½ +f?>Eŧþä¶þKØçèºÖÿ_Þ1ÿÏ{ú?—ÿ™ á?‚‚Îè²�h™ç�ϳP «aM@CºJsœ(ÐGYÿ‹;×5„y©aŽoõ"ºÃ‡ySt™xº<×/FFÍ™?<jò¸?º‚#¹‹cØ/¬êÅ¿ký/ù<>œ–ýÖ O÷L‡!$g´,bip< pˆÿœÈïò_òñ?$ü74šÖ ŽÌÿ*f½$U¢uÀ14R…Ññy/ÿ“.÷6m¥‰Õ®zþlyï¼þc¥ˆ3m.wÙJ…y¸†á‡`M“ùþU§<Y¹•´€"]—Jºîé aÒÓ›ò`"Ÿp´ü+eŸëç},S.¿˜§Ÿ8Úþù°ÇÿQhcAƒ²³ÿ9NÊ×ÿ? )ÿ1Ù!äi`paþ#@ j@E¢¬ðØ8€*Lþ_4šVØmÞÙCMì6Y_»æÌ…J×c!2éN0¬µAgóÝ)×à²S»¥Ê¸ä5Ö÷ ôœê‡ð:ÿCÓ>’8<þ3»ü—¸|þ?¤þ?*«²´ÀDë˜ÿ<'�uª�ej’(kÇäÿxn”¬ÙãUëáJצ5ÿ€ÞD{œ—«Ý{·quÁµm¶Þz´*ÑÅ6ÿI§Ü—¤ôÕÑ?¿wðÈÿ%häƒÿ^æ¿í:áèh +ÀwŒÿ¬˜ÿ™ á?¯éŠÄ@p’$^ÐYlÿë +ÐiQ×5hÈ:óîù¿÷ðÿQº‰JÝYmÐÓ;õéæZaôÀ d@®<N:uñfXŸUhT{æÜ)·É_#Eoh�9Ûáeþoì'8È~oüÇ9ÿ³@êÿ§¨*¯B02#žÕt�‘’Ì"V5D(Âwûÿ¿‡ÿ5iñdjýI—G…à)º_€ÕbѺe¦±ˆÆ\ÐT¸Çñìï¼5îÇ~>ÞÀËüŸ!tÄ Àï±ÿ¥|þ/¬Öÿ5ƒ“0ëY™¦e¨´Êbû_– +4¯uþovSæ{{?˜U†Sv:¼âmP¨´¯–þ1Òb²Ôg¾Sð&Fÿ™ÿ¤Snó¿‡K^ýïõröÂËü_ ègÉÿÝõ?ÌÿÜÿ7$ü ƒŸ8Ë�ZçUÀÓˆ² Ë@ç!/2:Ë«’~Lþ7Æhb6£¥¥Þ7Ù<n\o¾5ìߢ»§iæõêPk.ýù?Ò)·ùßÇ%¯ÛýùØÿ¼ÎÁqò_v÷ÿH¹ÿ6Hø¯"(H'YU$lÿ +eh�CP4•c%™§á1ù/Ü{pVóoî.îJ·Á÷½;HwÊ•«Í«K£³4¯Ã™ewçÚ6ÿ×r_Sñràk!_x{ü·ÜcnýñÍügÄ|ý/#$üg$*º¨U¡ñø/óXÿ7T ÐÍhÈà°pÔù?w(½‡Q1ºTok3®äšÅ‹Çñð©½UŠ[S»½ÁìV3êª{’ì½q†kÆ—“ãÂÂÐ +6u�ëWªB¥ò¥ÿ×°Çéfdgêÿljì^ü)÷ÿËéú¿Œx$(‚Âbþ#@CÆæ€D#^Ñ•còßYÞ +:îõñÃÕÃ@îX1ª"Eâ,ۘؗëºXSñÉü_Ò)÷$@R|@äÞ?oaÿÁÈõÃã*�‡Çÿ=ÿ_6÷ÿË«ø?ø§±@£!Öÿu¨�Bp’¦ +Ëqº(“ÿk‰‹° _9Åáüªª\²Ýòu?àJUE~‹‹VP¾±-õq–nì!rþqéöçÔ1ÿ‘šá"–�¿á3²MñŒ¸D“phoÿ‹ûö>ÿ— Rþ#•Áž¢¢Ð�kûP8YºÆ /©Ø8øþýñž×~¬‡QGóaq«-ÒãmctW½©>ºêcóZŸ*÷½{î6@f”lõÝêŸ+¹ÐZã:DáEÜ©;¥Ûn5¨VAs>û±ÿi°Éÿ©‰f ÿòŸYV&ñ°²¿ÿ'ŸÿË ÿ¡F4.lú"ƒùÏý濨I¯Ê²`H¯ð?oß�¤7r¶üðÊ™WǺT…лŠ¢¡ÉŽªìÕ¼bNáC‡— YXwÐçø`µãïôßD.vñÿýàHÛ�ëÿ{û%.ŸÿËéþ_ƒá Gs@VIü?•€jh"PETŽf™×âÿ‰ÿS«¿ôﱆR¯y iù|P[O' жæ +�·õê¬äwŸù;èzìo·sÎÞà€ ¯&~±ó?$ñŸ§wÇFòù¿LŽÿøÁ‹¼ÌÖÀË,‹ù&Š@×9±¼ÀIêÇò߸¯×ÓAuYl]€™×ºT²�¹û±3R/UÑš¶t.‚šÖæÿf]ëí¸Šs=à óßG~]D�ä?»kÿc€ÎùŸÒõ?]ÃZ>§–À#C²k¼$H:£0òÇêÿbµÔh‚ñÛRuõ² ô{MQíÈ%£ª>ùÅþM£]<ôùÇÿ¤ƒî +€¤ô9ÜI`:^”[xƒÿ!‚ÁfÿÞ3ÿÇíÚÿ<ÇÿÍiüOƒ%‰�¿ð†¨Å` Bˆõ�È+ºôîõ¿ï¡m©<õ|fÒíßÐö¸U¨–_xh‹wÓð‘å‘3=Þ¢‡øgú'ýsÅûNr”sûÛð2ÿãùU͵=¨a-ð0ÿ…]þ‹tnÿg‚UüONf ‡=à5Î�P7!k¨D’¿%þÏ7ÏÿKL©]ð·õ;m‰eq|Å_Õ-æ¦Ð™À‘ÝJñ¢Sè- <|žÿOûçJ�”ÒÃ|öÿ[ðÿ?sþŸaòñ?$üWÙî+@t¬ÿË"TNÆꀪiÒuQ™ï×ÿß!�ìæUe`¸ý±íÜ·}¥æie¾Ô׃Š×` +‹²>(“eXÕžÀÖüqjš½<ÿŸ‘ð"'0Ýã†ý^ãÿžÞà¿HÑ/òùþßLðÌÔ$&EŠâÏÃô㶩%±6SúrÃD–¾úh„£ãˆuÎ0Â_JHðüô³Ëòi·Ñ$Ü]ò9Ä2ùD˜»*ˆ¿¦ƒ_Še l,NLË$×#¡ððã?YŒ£û¥ü’}G#‡„[½b˜n¡qÕ‡cyN‡‚QkÜEcoÚl–ó[Z¦X»èþé¥LÌÿ84#‘èøEÇñ ñs˜Ü)ò}Sÿã‘ÀñŸáøÝù?–Îçÿ2îà¦cÆ™jÞ¹GïK….vA3ÛfcÆõäéÌë×TP\�0¨µ+Cõé¦Ûé×þô}?~ÿDÔŸ‚•~ÎøÏìæÿÂã?›ó?øîK{ÛÓómö#Yæ`5ðCfVñƒ…nõ»‚Ìß=6ꥶÑZÐåpò¨Ü,/UAÎÙÿoXûtþïîÿÃüÏõÿLàEØP¨0ÔFƒÀ\býX8$œë Ý)4‹K«î +,™—W’zuqw[5ébÿéqz§_8RidDt.þìHùº>¶Å’u?ÕÕÙæÿbwóÿ‰\>ÿŸ Rÿ±¢$h€ÉþIU€‚0ÄSg5E”ßÿ÷åõ¿ä#1êy»®ôÔè=Aó6’Üö…+·|ãÚ?•¤û«Ëv¡dßox‘.{wå43n\Nk‡ÏË€ÉGrƒ™b‰Ù$ßs°þÄJï?¹ÝíEôuÓIp}æ$ô¡X0„ªµšWŒ=Öû <äf^ÎqñM®ž›†;ŸkRö.‘éû's"<üä?Ëïéÿ|îÿ— th¾é‘ €8sÏš qGL8C’V‚aåSKjé¦4ÜiI’›ó„/Á9éLç¤Rp(ßÏí`†³ÛPõ[N04™i‹«‹×#ýéá&à§å‚Uñ"W!>É”íYºù׳ éÈë?¦(8À†¡wãŠ"“ëÿ™ õÿ§5]PE ^�/"ÈŠb�•Eª!jš$£Æÿ©_ÝCq2Dw£úý¶Ø²Ó2$»Ýšú\j*|û¦6~P›'É€~zGºf,™NO’Þú…zî®ëL=}’téXI>âðAXÐA/UJâ˜{çâSÏÉw’ +qáÖ¯%›ùÄã +Û +ÄúÊ‘oí]—}Ç•v®m^f°Ôû|y…®·wqé¿…ÏN]–ãHåÿ‡j€õ?fWþXÌåXeV¼ S¡5ƒ‹`ŒÜÙ`]O¹¦¶¶UÞóón\ãzzq0ð£JA*ŠÛ™cºÚ£j£‰PåUVŸ7¯Ây!—#ŸƒÕ2ýù^ƒð^^ç?áË6ÿY‰¡„¼§5~rþ¯ß?6ß>êßþþ9Žò÷Ÿ6ßÿ~.X¨Ûæw +;4þÓüÞþÏã?e‚Mƒ"u¥,—n¡5ó_ÿÏÉGæoóßr‡™ð/þ“ æþß™àþŸ^9¦fºX¥Ç¶~@D@9í‡ÅÛüÎzÿ‘û¿iÞ‹ÿÇÊ/ñ¿ó¯ÿFV,�¼ý7`.�~T¼Íÿ8C}ãÿ.ÿi‘Éý?3Áü¿D¾m9!rÞÿàØäÿGí=Èn×þg9:·ÿ3ÁjÌ/E„ïdÈ‚ú¦›ù?6ùÿQ¾ ‡ù¿ÿÎý?³Aê]ÑÁ¯Ù¥Ó¡’·ŸÓÿ'Á&ÿWŸS�ÿÉ&þ+ÍìåòüïÙ ÿ/œ¡e£œô?ÞäFñŸ±½¿ÏÿÜÿ'¬øxð_ÿ×µrð“á-þGŽžIü'šÛõÿ“$>ÿ3Á:j*VýQà!Í4LdUþìË‘ ÞâÿrþÇ⾯p˜ÿ{óÿtÿ5¬ø_wC +zž…ɯZ(§ÿÏ‚Mþ'á·Î FvÆ{Áœ¢AZðÝ¢à;æÿ1ŸÿÏ+þW#èëÐO3(™z>øsàþÛÐ$}�:ÚòúyÆ!þ3{ùŸ%QÊíÿL`£ ˆcœþ30C„m€ð_ÿM‘]ᮃ´ê.å¹>…Ÿ‚Y´a, bÿ�÷Œºw)²Ý€BeGJõÑQUâ<Œ¿@¾D”‡ÈöPx–‹•?^â?‰yÏÿ‡çÿöæÿùÜþϹÏÿÏ×øo¸î1R?Çøfûÿ9ÿ³ÁJÿ¿3Qîïûâ5þÇó7ÁáñŸß[ÿcòøO™ ÷ùý¹ñÿ‰˜iüGšöâ? +ùü_&HüÿÊ~äÁ|ñÿ'Ä&ÿcOÿˆôíã¿(Ñyü·L�×YØ.‰÷Up°åoÚn.~"|”Ïï&ñŸÛ‹ÿ.ðyþ—lÆ–– ÄH€Wx@‘e"ãF‹4)ê1ã?ZŇ²§—ïš¡ot®›#_¬[×׋~Ûm]ÓPÓ§—·Aâ Ïø‚;ŽÊIZµ–²Bÿä9Dáv¸æÏ~¼z|”Ïï&ñŸewõÌÿ|üÏiþg^h1€G +‡ÿCPˆ€¢@É«óHÿ6þǬ V¬m{ÐyƒµLÎÚÏÂGùünâàø¿ÿ‡òý?!á¿&Š"-êà™¼J+�BZ¬�%–áI„¦Wò¿~ßøê¢u[º)þÌ9¥Úˆ¥k�À‚ænT³bMeš-ß”JÞ4Í÷ìè'›žŠ©£2ÒO_)ìZ¤äÉ^ßÂGùünâ ÿwãÿ0V>sþgtüWTÇ�`E ž•y è‚ D«´&ª9nüwN(!£§:Kh.BmÈA ÍçËAµ3fWã¾>xìh¶ÝáC¢ÿã^yòš§ò+€ËÀ{°æ¿ŸéE�?Ìÿ=ýŸËõÿl€_;aé©ãž~¡•$e&AÀCà…øósu9#èñg«‚ä4Mv +lw§@#lÜ.qvZ †;Ú(x.9/ªÖCR?â›*ƒzרGÍB¯Zœö»õÇ»+ÓY†a£Þ¶ƒ²–}¬ùï ¡šðyú¿Œo×ÿE¬qæüÏ +‚4÷“}h£8ûªWœ¤ùK7òIІ£ç#Ãœ“/ÄPšŒaVŸž&ƒØøÇŒu‰3ùËðÇ�YxèFúàYµ ‹Ïƒ”è˜] »«Î•Þ‹½§¥tT©fÜ•Laq×{hÏufŽnµ~¥©æ©¨_Åšÿ$ó×'åŦç~þ×<þg&€VLp°¦ü€lþ𠬑o'ƒÂíB’Ê(ádäÇ)_kjctI" n·º¤ }•ÛhU ¬›IRm^ýí/’ +iÖ¢íR°JgBæ©nø]í¾-žô'7ª<ݨ~]Ÿ^Œ·3WM§Ò¸êD¥ydò¶Ñ©7+Ið£3@à?ƒ‹7ø/þKRîÿ“ 4¨Ð ý$¥Wl+¯’š¢JVÛVóòc8…IºÈ,+fëï¦DÜxª<øÈvq›ÐÁã=éuÄ!‰#ÓHdõ|wŒÛ’Ô©XýëzŒòÜÓ/ÈÇUX'Á¤%ºÅ(½/ççFèé±CÓ™ë㤔ÁùÆ}ç³V®®t~ø’.>1ó‰j’¤l5—Ï‹›g7ܪ‘¦P‹<<ÍXºL¡5Ðá"HQ¨¥'8O×7Œ+š¶çúá`%‘’–ß@·O3Ù+4¯ŸZí0`[^U‘P½*N[©2zªT¡Ô¸«NrýègEœ>ù“óÿ2<³+ÿy!ÿ ˆ€Ž÷ú ²ߙ×mÔJmí¡Û¸m÷¯Ê +ðFãÿĉŽãÀZ¯ÞµÊó‹§Çe•éæ’åÏb>èØ*ÿxÔ‡gâ?¿ÿA$ñ_8:_ÿÉÉúÏ«Œd<PUY�¼!p@1hMÐC:Íóï]ÿÁB#Éž˜M€"ê€@yœÓã2sc„ðV¯rÔ@[+EJ¥\ãçC©»×Ýur¯í÷ÓõòP9.MãX„§'IFóø<+šÎ‹ß^±ÉBÒÏ“ÙöþÛpB,S›,Ž#ò_`wøÏp|>ÿ› þ˜ĨتÁ«€—Eà"0íyhðŠ‘ø‘ü*+^·œQõz“i‹îõëâ`T{Dñ§êÔŸ,=f±€ô3ÿ÷ûéZÔð©• ’*¯K‚ÚùyÄÁküœãI€ƒüé]þKt>ÿ› þ«:##r€–Xð‚*sèø@ñŠ*£ä¿U®)öôªZˆäöLöC•|Û—Âý_¾›÷58›¨7º8ÙáÿN?}Y¬*»mýRàþ{¾›LÔË�8<þó»ú¿Àåû?2AÂZe È�<+ +@Ñ ¨PA~+HÉÿ:Ý*;Oæ ¯ß¸uÞãÝ;¦ÛÕÞBÓ/…Û.Rj ax²Ò}æÿv?]3ÿ.)^“?t)ÃÇŸ)’Ìðu °ÓÚÏÀ}‚—ø©dÒ?Cþsâÿ™Üÿ+$ü—xÝ5^†¨cþ Èj@yÁE™Öµå¿4'ªË;ãGи՛Ò]õ¡Ú«Œ{æJjzÂhƒPñµ–[ØàÿV?}æR|x`çû?ã·ñQ1_7qÿ[û¿Ä8þ»”Ïÿg‚„ÿ‚*(4 Ð!¦>ÏsPEŽ’ åùåW¹¼`y;×f¢÷h· V⢬ª#´¨ÕAñÆê÷z—Õá3ÿ7úéšüm\v˜ù›ßü9i¿Æüœ#[�‡íf×þÇ9ÿ³@ºÿ[ád¤J`d‰Áú¿®™a4ÀK¢ +%NôîýßßÃÿ뢎#jÔÂ@¿Y'ÖÃÜmQ£Ô©…͹5YØcÇ´1ÿ·ÛO×B …l÷Yíß~— °×àO ^æÿq-€ƒüçwí†Îã?eƒ„ÿŠÄ"…3À@‘°žg±þϨ@dE]ÀÆ- +ìGòXm>˜íÀ›U-ÚZ4ÚâlpYºó¦ÕÂ]Y3ŸÊõÛʬ×Xè÷áÕÿ_¶�º«‡5½6~Êoa›ÿ$Ô N�?ˆ?G�žÿ“vùϲ¹þŸ ÒýŸŒb(ºÊ¨Øþg ȼÁ�FYCåU^ÚwóŸô¤ü//Þ²ctF]©4g€S”§;ãB½½]ø£‡aÓ¿¹»ì‚{Í'ûÁ_ì§kP$g¨p„¨Õ“øþ¾’ñ=$Á+ý¢àþk$óƒuDpÿÒþúîÿ— þCH¬¤@€<i€KÀèX8‹*CåÝû¿¿‡ÿÎÔ¿UÔŠi-‹S›UÇ Åo:•²W,Hz±Ø·Gív(ÖÛ Ü}æÿ~?]€R|j_PPÓÜÈ ß”/´û㊂ø9GÖ�ÛÿÂ.ÿ…ÜþÏ©ÿ¦êˆu !E¼�ÉÒŸˆ�Â|‚š„d‰ùþõÿwð¿}t‚×ÒdYpC4[ÂÂM2˜‰p°˜=<p†tÛq¤ ýÌÿúé† ~‡ðR‹?.õc¬ø…#×7—Ç üœâÿ~wþ_`¹Üÿ/ÍFñV#wù”›x<Ž,ë�e—"§r_ÞùëbXiMZZÃ2´à¡73®:Ëq™×Æ̤¥Ã‰/?RþkþñÃ>¬qˆÿ,Ëîîÿarý?„##׊79ûè)2}d#'3è;¦3üB1+Óôîiäû®O²´"Ò‡öBÓB—v˜¼¼ì/ç•vo¼¸,õ¢ëÅHGKŠØ¹z0š¿È…E¦HùOö§~ÿ9foÿ7Ãçþ™ 6‡ýÅsãÈ,†é!Ù§L¶)‡¦–®ƒ¶*]D¾ë¡óÇÈ7µQ²ÓÛO4þ€«-ÐEäÉóžé– {¥ùlõ®CõÞMU1Øj¯ùT©²VUžjKSŸ½…ÔsóÐõߌÿMÃø4þ3»ó˜ÿ\¾ÿ/Ä+dó8žƒe:(Xêñ°Ï®‹°}`ZiÙÛTezбáÅhÞLkò@¸ Ý Çr 6”nñòaH[u‘æî‚^’éÏ”ÿÄü<ý_ä÷Çÿ|ý?@ËrgÓ ùhy–õ`5N¯ChIp€x±\;‰£–`€{ŽçúD€9mïd}<°áÜ´#{§–f™ÃBÈËÃRW)/ûÆXåÔBãêéòv9º¥K£¢õ0BQ¸[¸J(çÓ ÇDÊÓ&Án†ÇýNppüßÝÿÏð¢”ÿ™`ì¡áà)‚–bKÂáÀíðÒ˜\_w;£KT¡éÄ}XÜݶü›~u>¼li‡¯~3§é¿¶øÿA +ÀAþ³â.ÿ9:ÿ3Éó;1±>ÔÅ3ŒüÞ%¼AEøÄëJÏ3ùÖÓ¬P3È(#Os®£Ñý}Sgùœúÿ6Hùo¹Ã!¶ð>'þ+Ãíéÿ"ŸïÿÏñ4>6ú§dÙ|dôÔ8àŒƒ¥¥°8û¥šùD/ÊÚ¸`hú\bg̲Ã4}.ÂB.þ °ÊùMëóæÿ„½øø_Îÿ,°ñx=%ï¼é„߆R4ºƒÆëa}ðؽ«D‹+þF¿ —]Y”—·õ¶µ¼qóe½?)žùOvJÏÇOÈÿJÓ»þ?¼”çË6 +‚x’îôŸ$ +1e”ù>rBkAÒ®!ŸÚìTQÁÈ,RE"¨“CW>£:#èL¨…Q†ë“¿> ol"ò½ÓmïÁÑ�…ÚƒiøIª^†sºÚÍ^GžµÃq%êËŲÒð#×èu®?3rúÿ‘¥ÎÑ™çã™]û?Ïÿ–Òý?Häx +@FŒ +x(C H‹4–ÕyMä”cæ« Š#™qƒò¢Ûäg÷tuØÔõbýi²¬ÉRp;Ã’^+¶3‹ý}ÓžùìÞ»áËO‘¾{Šï!‰L{éžÞšÎ$ |D¢Øë$q&½S´µàôgÏ·Åú3ò¿I{ã?+äã&H÷ÿBCAHU"Cð:-(Ëd'€"H´Â +ˆ‘Éÿ�z&Û/WÝ+eѼêzë{vÚÕ›b¹]Ò÷œQXt ^¸Müýãž¹b˜A˜¸äïó$æzƒ ñb˜?=Ó_Æ&ÿ ×ÅÆà'èÿÂÞøÏ‹¹ÿo&Hã@é'Z‘0ëu…2'Š@Œ&ë+“ÿÒ·zÂRYêC#ÈVïžnÚµ¦q©XíV»Á—Õ^E*T´˜ÿIÏ\ €Jr´Müvl½8Øô°c¹³þ]Øä?1ô>cÿ'î®ÿ“¯ÿe‚tüWEÑ$È,CÆVÆš€h�UG<5Ñ •wïÿÿ›lrcýºŸÐ×ϯDF]´š•ûbAõ–ýr³aöÁÄqå˜ÿ¤gn„ö5ÊSs}WõA¤„Î…À!lòŸ¬Jþgioÿ—ûÿfƒ„ÿ¢¬ +Öi@3Øêç…•Y 1Š&J¬n0<wLþK50½g–˼½¬3²þTh JQµ1–•ëig.o¬\ÕæÉø÷Ì•�èÄÛ¤'H„ñ¿R®"Û X¯¡ÔEº#9—{Hùï!?Öœ>cþŸ¡÷ü!ŸÿËqþÇ$ûó0ݱoÃyâ·KŸhA¼Ÿßó‘ç» +‚õ~žáÒôÒဧyRs;?a’ßäz>ÿýìoÿñ%œ‡_=gøuh_ÇúáW|¯ãà«©¹_ƒ™ñÕ°¦_µ¡ùU…¸¢õU·¬¯hŽ¾ÂÀûûÿ<7I&F4׬ˆï]7|ŽÂf÷×Ø‹-þEÿýüœT…6Q~ûåF©Ó¿»ˆþñ[ú?‚ú?~ÍÐBÿÀ÷OÕÝÛ‘£ÿvžþvžTQ]}«3ÿب~óþÑ¡x[$ +È<c·uKýåŸäÖþBÍ`@9¸ºAªSX Gf@%9+Ï~;÷pãI³çñÍœžŒßñ¨q'´Pœ-sð¼q“b…¥zX«,~ØþX »Ð¹ÂšÝÈ(=¥Êð¾E?•ôù¼7š=Aö¦xŸ»nþ<Hå¿|TòÇ_Þ³ÿ{Ïÿ“ærÿL ã +‘›Õé ¶£ì ÙfQ†ìŸšh6°c-÷™oYÕíX—R½>áEodÌ#ÆnwÙ)Ñý'X º%Tæ-åºé4rž42;¤ü'kÿŸæÿÅ»öÏäû?²Abÿ"£‹È¯‹þi�j +hM‘EÖ€Š¡I'I‚î&v¼4‚Ö ñKWgþ‰æÐö,t¦¹öI`¹C˜H‘•^‰Ez|L>ñé§8(óꜬȞ㯘ÎI’‹·áA-LM²m†Ìáˆdý&ÑœÈ3"‘èm ´<ß – +‹ŽÈ®¦YFá¶u10íâ¢Q†Œ‚Ö“^—Wœ'-ž~ÈÝå)ÿãä¿gCËUáñý@ñ_ wý¿AÌùŸ 8ÅÆ—Gm3m”DF‹í¨ó©£ŸÙ¦æ»k„g¤Ö:a}ÌÞÈ·VŸô;*± #?‰‡ÉLBÆ$QÕ<S#'ÖÆÍÖIl™†©¥¡ÖÒ«;\'q›·ÚJ î—÷ßâÛbAñ]þnI¨VlÎaÔ|*ÇKK3µÊ#[Äå—ÓŽU*þ{û mñÿ³öñ{û?¹|þ'ÄãmÌ‘5;È™ ‡h5¡þPSÁB5ÅúòzÌë¡Ùdô¤џëe2‚0™ü[Ó㇠þq 0ÄÝGMæÿžý/°ùúo&XGxˆl›n°3$ÃçሠŠš©’Ô*6n¸|¨ÝžìÉÒPÕh®>µK°x»¼³¤Ñ}¾ìÏ¢H–ë~Lè‡çÿ¸]ÿONòñ?`óà:Öb¥D›Þ îhr03Ý}¡8"b{u:9ZOŒ—nߨ-ÑÓ–Ç7¶rמΠÉiÖ‘\êmÀ©Ûk×ëÅf/È%FFˆùÿ¡»?ò_ÚŸÿãÄ<ÿk6H“Ä<õmB]²*ù…úËÆu3ð,¸�dîï¿~ýÝùÝ)¬Ö;É®ŠäÛÉ^¯ÕvÌuJEÈ¡l¨# +†Ô’Éå/qg¤…¾áslÈÏ^lÀuÒ¯`먾‰Œÿ".šej¬¥&©Ä̓r}Js½)„ŽŽ ÿ€¨±”R¦ƒï.nUõÝù%äÊÉoJ® pëÿE +ëÎÅŠ,ä&'¬Ä}Ô5¶ïÊL–s}4E~`ªŠKg}/hî™> +ð•ñ@”Ž ¹'ü¹§™iYøÑx~0¦ßbÒ ¾%=n +�jóQQ!‚ö_ðû"uŒ4–³ÞÊÖ}®ÞÙs÷Â;Ûyd>6f®¯ðÍ¢ð[_xü¥x3ͪçÍ~ßÕ,wHÙ1ß/~ü€„,ñŒòú=?¿*üü(2æ‘-Œäñã ‘_—Þ¹Vü¾H,ÄxC#>c j6B>ŠËH+äa¤“§qF]…ë®�ùïë §ßÜZ¿'-ÆLÅ?tËùù½½ÁGC3I¶ƒdÞÝGdÃÔ»zE¼]>o·ÀWÜn=îi‹øׯûGÚaΨ¹ÄÏ(~ÿ©q½}þ×ø«qãøâÄgfóº«Çø|㤘°ÎˆÈŒ'¾[Ž“\ƒüÒda&ùÑñ“ÿÝY]çyÈÏ—}§œqvÞëë/ñõå¸Ü¿}w +A8ü=ýj{ñªµøÍo’¼÷üY{OÚCtbµ2øvQD|Iñ ®»Ï±{ –Sžg¥Ëñ6úõÖþ÷ôN¨ÕœQ òŽÍ1.9…6_‰4DöžA"íñ+DD½ŽSQBÓ‰{Þ† †ªáæp^¿Á_÷;įqoJšKŸkÜG>î}P[ÿx" +Öàïo¼¨dyûuÅO.~þHßyögÏäCºÆ=÷ƒJâŧú™¾sàëoª:/xI¯X5øÃë;Ÿ%©Ò7�>ObQ[qÿ¹#©©{ù7v£µÅ³ÓÒæ¾Ñ<øÖŸ‘^åùW$öÅñ~ƪ½þ«Ëüý›‚Øü5×j¸SŽm\<E~õVsmO/W£§¹b –×a}jŽžôÒx8žÿñ]lÎâáÊYØntüU ƒþ_ûþ"ŸÏÿf‚Äÿ‹gU‘ä\ +Më€W%@I•išŠ8Äð¨ñ?ÆÂò¶ÖЯ‹O%ZMÛz8ºHxþuQØÊÃåÓÈð®—nÿcÕ7×›�«’8œÇiêFö,˜Aª‡¤3Úòm3°²{£�êË+ÏÉÁ?û|&6øá× ‰^ŒŽ»tÿÌ.ÿEžËãd‚4þÏh:ÐÈ^”E ³†Q2¤(ª®©Çä¿~Mß²åz{,<‰÷…jÝ—½ë‘ýÉÕÝìÆï6ž/÷¯þoöÍg°Yº#˜\¼1ÿ?ÙÿCâ^ðÿÈÇÿL°Y¿Pë!õ$öÃ\ïî˜áŽâ’Ø›s{Áèy£ßֲªxÏÀÞ9±2˜ÖyÁvMõ‰WfªWí½=ï¸WkoŽ)©±:ÿ…ššØ´pýT†¬ë¤?ßFáÈÝIDÿ–õ•1ÌÝ?Yäiúùlú©§M|áo0X|~25jôUïá¾Ö},/%žïkŒxÑqÒ㨯äIµ1jÛ·¹ë]Ž9räÈ‘#GŽ9räÈ‘#GŽ?7þ?‹ÿ �¸� \ No newline at end of file diff --git a/core/tests/fixtures/config_install/testing_config_install.tar.gz b/core/tests/fixtures/config_install/testing_config_install.tar.gz new file mode 100644 index 000000000000..7cd14a2e3a67 --- /dev/null +++ b/core/tests/fixtures/config_install/testing_config_install.tar.gz @@ -0,0 +1,43 @@ +‹�v)æX�í}érêȶfýöSÐ'úÄ>'¢Ò[óà¨8}Æ63ã[7))! L]ý>}ÿ÷œëLI̶a︪¶ÖRIjZßÊ\+× ZŽ6ºV£¿~�½‘õ±i_/ÆÖOç"Š¢ŽËÿ4%Dÿ)&þ‘Èš¥Dšc(å3-pûS†:Û¼C!¹k|)Êl6{¯îfïo%³þÿ'¡04õ›Y^ãuhäg02€H…€åy¡ ÇÓ´|eA»¯9:ºÉÞ~Aèßd/DW:r‘#[3‘s•ÉhŽm˜}ò)“áh|=Fvx1n;zh¡ÝøK0@ãM+aÇ+Es¼¨IG@‰WÐÜd +ÝlN¼ƒY#{¼ìxÑ–rÌ&sSe›ªkOnÁ ÊiWäF·Xü*>YrõMÇÆßL߸§¦çW3döÁM†¾r=gŠy7;´¬+× +û&îþ%¾t…Üœ¡è&üË•‚À´ûÑó g~»g&cAY7™,ùnú|-¸}sÒõŠº*ºé»\Üd¦¦oª"ДŒAGOÊ ð³¡®¢£¦e¸çÿÎdþÏÕáûWð¯zÐÖñ•ŸOÇ?»‡‘¹ÿ— ÿº@3¦iÀʪ +8(Ò�ªšD±š†(†…Ò7àÿ\è6šÏÕœLwæ…éme´Ì7¥—œgF^(NÔ1bú£"“í‡ÜºWü:ÀbD‘M½…쮫ÁbÈ úõ^kDi™Ê¬Ž~9 Ò_(Ò/ô‘âã+–Ów’ǼÕjCrgû>îí¤ýðÿôþñGh)P#2È?‹8†evñÏP4-¤ø¿%ó?Ë©’Lñ€Q)p"±0T9Ä°”&ó߀ÿozÝ0Æ Íµ–|PvÇO~S¬"?´ö9:®}o熖ôP½Ýú§¾Žv|–�Ù›‰Ðo~g´7àþZŸ Øëž9†Þ"Í뙤×.èÉSxò§Áõìôþè΃þðÏÌÿŧø¿Åø‡Çð"žÿi“�Ç1P9ÄéG³PUã#ñ¯JPÕ[«Ùãý‡q߉œëöÛ]™½ 8p{_\TÍÎKE•î{ø8õDô3GÐõ.ö·{¬‘߆ª"ÌI¯HJ¬goáËÔ7-—¯á¿aý_àéýŸNñJð¯1bðP§�Ç«t•-hºß†@©ß½þÇËSï{„×°Zv7§Íç•r±Y‘|5T*ÃÖHsÙê°Ô·Ÿ< X»qWÞ +ýoÓíß äºãáÞ6K¼cÛhŽÏš Œe(ôv—ÉcyC¡¿”8Äÿù>ì£sMþ?‚ÿ}ûŸH3tŠÿKPŒV $šœ„d ih^5dš¥9Áø–õÿ¹ô¾RÕG¬áVæÊ|žkkK¦MÓ…Gîù¶Cßñþ%åüž^ô³[`ÅÀoèÿõA>ÒO6¬F|ß°×kË=«Ìêøéf€Cü»ø•À,t. pÿôþü/r©þŠñOC(#J�MëàH«0ÜÎɺJâG®ÿ¥…¡ë=ÏÑM;KήóSÃsÁ¼1Éç*¶iž˜ò=Ýßú†SO\þ³o}3Ô?è°wÛ<„ï+êóW÷èÿãXgœü:ÁþÏîÿñLŠÿKPŒYçEI x 1‚�8M–€ªrà×D#jºø-ø{ÿ/b®3®æùš9|Ôs4èôŸç RZqui:¯ÞŒŠ=~Ѥaop7ô·G|çÐ^ÙÕ‹?iÿ/é¹–$5¢+ø'~÷¾ßŠÈÓ½Ö±z¢Ž7†Áµ-K…XœOÃ?+ˆûøÄTÿ¿Åøç°Šo@^Â3¤ÊNåY Ê¢ !RœŽCÓ¨SñsÜû°GV‹/wŸ½Ùò ÖÝÞK1ë3m.u˜b‘~ÙšámNc{ÿŠ)¯Vsn1bV2éÞ%ÿ=äZæ—+,¤'×å @iáçÌø«þµ‡ÅÊÝ îöÙýCøc,gpÃåô–ÝǺÿ)ŠñoˆUÙ�<Å‹�4€¤ÊàdYäT +Ñs²ýïüßÖVÐiÔÇ}Mè4O»çžMÅBùÛû!šT» hF[ =Šð¿fʵ�¸kW3Üòä{`ôçÇèmüæøL2àøü ÿ‹<•âÿã_â9 éšhAc�GCÈ"þªëP€*¯k:{²ÿß)øμ5{)7ŸËº6x·�tGÚ˼Pê<9µò-Û3Õæ‹UowñO˜òPÖ7§þHüÚÆÓþ_Kgþ=zÿcÇg[�|ÇüÏRéüJìòˆ¥€ Ïú¬Ä`üë@´l°«ê¼Æžÿ/âC˜ïÌ*JWoW§šcá3«H:ðT|µ«ÂC¿:+R¨²ÁÄ”»à¯¦wV�)ÚÑëø?ßÜOè(þ¹ƒùŸ¦Óùÿ"”øÿÒ¢Jé² +h¨I€C’.Ð� š!qº.òÂYñ_Së:¬0h.xWÖ½,,åræ`Ð)еE8,ↆ̾lÍÿ‡sû½y?šðÓùþ½ŽÿBg4�~ÇüOÓ©ýÿ"”øÿHš¨!MPâ Æ?C‰Sy@ÉxU #Q¦dýœøŸ=¸®ï<)³bÊLûen¬e³ÅVyé--.FK}æÙYwdô6ø'L¹‹ÿ.ny{öÿµÛMÑŒ^ÇÿAï’øçñŸÚÿ/B‰ýŸ•$@q¬Š×ÿø“ª# H<kè¼Á<«ý¿6D#³>ËMõ©Á(Ò°v—{”¹f¿÷ˆê“gqæÕR_k,½-ûaÊ]ü÷pËÛz:÷Ÿ@oãßB¾ž€£øç÷憦RÿÿËP²þW5 1œXÊxþD 3ÿàeÁAF”OŽÿ;ÿü“gï¡~[Ïœ×sëjŠ5Fï›å;£½45î̵]ü¯™òPCÑnà[!Ýx…ðo9çýè›ñOéþß…(Æ?528h¢Ž�§©4ñ[ªÈ‘ ¬çÄ¿Ó—üîó Þ©•›wÌÜíË°?i-Å\q¬©®2{ÔŒªê\ű7vøB<bÔ¸xZþöÀú9SÌΤûþoÑþÇH7ÃñEýÿX9ŒÿIýÿ/B+ÿ?Mç•"KI€SU@Q`%©šu‘è“ýÿOÁ¿½|äuÌÕásùY‘ÚV[‹ÙPYklŒÆw#ë>Sö²±ý/fÊ 7‘©ëÏ{t€àxÁy�Ççÿ}ý_`ÄÔþwJâD†Aº(�(ÀK�äY<Å"•eXY8ëþÖZ¢Å"ÈJe;ן—KòÓ)Ü÷|6_’¥—¡°hú…‡±¥¾Ì’À”ðZ ?…þûáÙ," Œñ¾&±‰×Ä%ÚÇñãõÿ}ÿ_¬ÿ§ö¿ËPŒQ¦1Ðyˆ’Ì`ýê@¥t0†*ÓÍS¢~²ýÿõøß#‰~¬çA[sy~ñ¨-âËcmP/=”^õ¥q¯²Å§îûè#3ŒC}wøs%š«ïø…öQp1u{á¢$ÖVƒÚ`•4ç³û†¶ñ?5Ñ,Æ¿Mþ¡e]$ÿ+æÿIí—¡ÿ*/PœFâÿ N–x€þPÒ4§‘úÆú?žoß�„›Ž9[^P¶ç¥á‹.– tïùšlƒ°o2ƒSžÍiз©à®æÇkÝDà/«ˆ¿/¯ˆ€è"R°OïàßóÏx|ýÿ/ò©ýï"”øÿjš*Ò$á'¥€3x@ +ãŸF^�ÐPRúPüOÞÒžËrµâ‚†åq~qhMš ScÍá4«¥YÞëlðt=÷·Z)濇ÞÁ¿ § ¿ØùïÇðÏQûó?Í‹©ýï"”ÄÿpXÁ7(h2-cükFªÊœ(iªôVþÏ3áßx*÷S¥´Ì5oÁÌmÞ©F‡ìÓШ·µç’`M›:BMëmð¿Í ëu@+jÌDé:à(Ç¿‡|üº~�8Šf_ÿg(JLñ ŠñÅ-£‰Yç‰ÿdê€SuFçd•Ç+‚Å¿PÊ×`Xgšª®Þeù^·!¨m)o”Ô‰—ë=ÔZyå¹Çݲ/•üǺ/�âÖMºÓîc¡à†©r°Cïà?@Ð?ƒõïû¿¯ÿslêÿJô 2ºYÀCŽ3X�yœ S†f hÀï·ÿ�ÿÊRžt=zÔé=Pãa3[*øÿÜêÓà…ሶ�wÑEÜþ1®pŸÕ4ÏÁ�NÑýMô:þ#ûªæŒ]¨a/ð;ð/0©þZÍÿB’f�NÓ5Rÿëÿø…�$ë’¬ÅóôGÚÿE:ßÊzÊcµ®õC¡ Ë\¹jÑÙöÆhÑ)ænÛÙîJýý?áÏ•�È'_Sëÿ·Ð;ø¿˜ýŸÃxÙ·ÿ3iþ¿‹PŒD-08ˆ…€ÀÒÄþ‡…€ CžaN¦µï_ÿŸ �ÆrQ1œÞpl?µ<¹âj.߆÷·JÑÑÙEAW +þh”•�رÿøh7Ýn* +ŽRŒÿy€lßtΛö{MÇðϳëø_ŽgÙEs‚®ÿ/B¤Æ9é2Ô&_ôYÇí«ŽM-뙀ˆ´&²ôÕGkÝh$ƒ%¯³û!þQò$ÒO>Û‘<!Ÿö3ßÅŸÌ ñ'‚ßU›¿–!¦ßŽE²àáGµN"eöK’÷ÅöÊŽ &¦ô4¡éN¶öVûCiN¼Q©Õá;m4”åüÑÖ‚ÎUn;»ò³_ì‰á?JÍH$:~¹Q~BË´GŠ3Ežgê¿?ø1üÓûùÿšIóÿ^†0Ó›¶Uª9-sÞ³*耆?›µÛ•¦3·WQAn€Riûêä¡ÓîUþ”xøÑ(ï׫d¥Ÿ3ÿÓÜ~ü'p©ÿÏEÈsfXÚÍ ™<ßG?’$–|/ gEÏ_èV¯ÃK\ý¥VÍ·Œæ‚*£ùay§òRŠþ?Ek·ÏÆ?ÏàŸM÷ÿ.BnˆUdE…6P|s‰ÑÊöýˆjg¹¥Uux Ì»²¨–oë%“Êõ&/Óº~k‹ùR©ø£S‚ÿÀñ°þïû©Ž¾¸lý/f¿þŸÀ¦öÿËP²ÿ'”DPH¥Gi¨<Ë�^¥x"ƒ¦÷'ôoÝÿ‹?Eþ˜'p®ãˆ“ZwÍØPšyÙìñe§ðàZ,7ŧò]+›?my–½ŠX9©Œ5$fí`³ $W¡ÌÌ�K¼pLjñm’õÇ*PrýñåîFiÐÓMƹõé«Àƒ¶oÁ�ªÖÊ¢yã \äù¦ŸœÎv”è"WÏMÃÌ猕}«S\ôý»ÈOÿGñÏÄÿsœ˜Îÿ!ùšgºÄ�UîY$bÔ¨¾„Ý'U%hFº2µ¸—nzHÃL»ˆ+Ü|ñâ%Ìô•tòÕûy,ú¹?{äA¯iûŠ¨ItK}]¸è“矛–„Ût ñ¡›i¯“à_ׂ¦×NQpÿ4MÔÿRÿŸËP²ÿÇROq<€2Köÿy¨Œ,ÊñQ9‰‘O®ÿ{Š±Z~ÈÂh¦„õAµLM‚&S°›†8nÕúÙ†>2×z¨ŸÕÆU<¡i“MªŒê¾˜[o2v]Wê¡©«˜¥£…HüQ‰ÒaAÝdQù<‹mjïĢƻ% ÛõÄ£»ˆõ™CÏ:8#nûŽ3í'ú¶}e%¨EfszŽ{pQëï¿„O[–Òy(‘ÿº<ºþcöã¿°,Jã?.B«ÊJÊ+2Z3¸ðàÌ”u?mà˜ÚZWy_ÌÍú°Âvõœ¢xa1+æ„öãÌ6•Žö¢ŽÑˆ/q*£Ïå`žM—yŸC«ùÄìÚ8cÞ×Çÿ¾ý—çèTÿ»Åë?†4R,0C�œ@B¿¼Š4ŠÑJ“Þðÿzký-7ÖÎÙE\ÅzcTgÑ +¼êŠ»6–Ï~&?íã?´õ³€cøgyjÿ¼˜ú\„’ü¯†€xQ�24ë’ +T @• Ϻ�%Ï©ÿ¡ê³`=æ +°'±v¾2`¨Ê�° ØÕ,ZS‰b +ù¼;Mü½m}íèYuìŒïþû¿5Ó0ÿýß_Þ*ÌnØÏ~ÎTÚÇÿrþûb½_£ïÀ¿®ÿ/CÉþAAFt@3"81@’u +†!‰œ¨r,}VûËç‘ÑUí%4Ög¡†æó¥RjOÃYyØÓ•—¶67AðçÃ\¹ƒèº–©‘M—7ñϦø?…ÖøãgúA€£øßÏÿDólºþ¿á×NPúÅv¾Üdl•8b#@�Ü�^b¬.`�m?ú‚Ùª!>£½†±³× 4î¶Ø{£úý½màoZŽìWºHì…\ƒGPíÕ°‘í–rÓ^§úR/›ö2¸ jÕÖØ/h©áÖø·Qß L|@ÈQü³ûö?bÒùÿ"ä#ßOö~]èÁ1Š‚6V\q•ì_øN裟ƒâzÈ0çäÑ”lFøw_¾˜:chÚ»>²ð$teAß’@úÆÃ[T/·Ërð$t'˱ßVÅŠQÏ›ü¢Þ}nͳUzŽµ^±¡¦NçßHkü“ÿOòÿ<ôÿâpÿÿ— 6uE\³VŒ ˜6^‘ï6'©wÉVfŒÔЋ\ Þjkv¹Ù +[Ë +d“1ôÕÞæªX³M¶Ù\}뇤C²k¹Û +VÛć+óÊŸ4îûBKŸ8aqòä£ê}uz;ôÞÃ\5íbÜóóÞÀäL]mÔ•Ïô•ƒñ>uÿ¦íÿTZÿõ2E\*~àÅ[ú‘®¼rjD +²û–åg‹óC8…±»ØžØ +þú· ÐCž,<4vð˜ÐÆ«�Âu[À!Žc‰ŸÇß]ÏâqÁߧÄu¯Fÿ¾ž£\çËÖ|äá.Ê&×û Ü›¯_À½Ö½Ð…Öµãõ#§4ÿëÖuù_ÿyÎôõø)I(äÌ#–ØeÓ\nv+¶ÚN°Ó#q¡Ý¨ÂB$]¦T²_ø«¨Wâ²A–Bþ×ä¾^¹`ÜÑ»Ž(+‰ü¾�zœÌ$7Û(ç7[Ï4›Ý’,¢jI˜6Ûbq0)**Ÿ¯ÕK£tÕô£R’HíSýéƒü¿O±©ü¿ õ1qùˆÂæOòë`j•ç|K{îÔ[½rAî`Xã&¬`Û6¬t««0¿¼,KôŸ3(þ¢8ÍÂ5Œ,èQüÖÉ-Dæií\¶ cøçXv?ÿˤ뿋P’ÿ›aAÕu¬Qàd^P“X r‚Êx(Ë'×ÿûžü/sjX ¬¼ÀÒýB +k¨©0•|(nÞÛ°sß¹UGåuÐÏ.Ÿn"[•�6Ñ?®öMûÕ_¯•Øx#éÇñl}ÿc8"©çGñÏäÿf…´þ×E(ñÿ@e–R€S)’ÿ“Ö€¡1<ÍCAU¥“ëþýšÊ÷k”ÜîhÚ¤º½ª *“¡"è´7U§ÞhéÒ‹¤6ø?äÓµ¨àC+‰»¼- ^çÇoá?´Ï'Žâ_ØÏÿG‹iüïe(Éÿ©ó¢fÈ4©ú'Žby Ó:/’Œ:Íò'û~þBEOË¥l(µf’€0_ãZž¸xæŸê^¡>ïip6Rta´‡ÿ=>}]¬:‘ûcýRàü»žêÎ¥�Ÿÿ¹ýõ?Ï¥þŸ¡ÄÿKgxE0M%þŸ¦Ve &òP’û‘ø¯RÍB®=18ýÁ©r.çt™!Õ*uš~·È>v\5ÕäMùŸ=>]#¿7¯Á8øzñç(ÃàÛ`o´û„^è£ÿñ¿ïÿñϤþ_¡ÿœÀ4+B ë^ÿKø48¢,‹*Í©T?tþ÷#Õáìáh(Né!_/=—ºÅa·H—ņÛÏ +cÈžÖt²[øßáÓ þãæ〽ßÿ(ˆß¥WðïÃé9§à¿þ>žÚÿ.B1þeižñªË¤þ!IUP£ +šd@A?Ùÿû{ðß‘ï|×_>ε™à¾Œ›P+BaQPÕZTª ‹8£®zÝî]©¿ÁÿŸ®ÁkÇU=äáU"·eÀö?¤�xÿ¡}f à¸þÿ—Në]†’õ¿¬ë‚1ô9 +¯ÿYAÓe`‚ÊrPÒúäø¯ïÁÿ}Î †¶Ö*¯?,ú#ëyîÔú¨–oW‚ÆÜ-ÆCÛ-¿eÿÛçÓµh¢±3Ý(�xõ?>I8ð¯ãÿ¼ÀQüsûú?†êÿ}J樳š,³�Òª�8†5€ªI4U•giŠÓ8^üHü÷Kg³å»³’EY‹ZK˜)wùº;-eëÍœªÅY·¶ÐŸ‚òþ_×�:«Çu€ƒ1~�ÈïÐ.þIy%*¡DÏ#�ŽÛÿö×ÿ4“æÿ¾Åøg‘ÊIPÇ€7h p‚ cM@Óñ"@3T]G«~þÏÊÿwÙ6Úƒgš*3ÀÊò¤nܪoðÜoxõ'œuî¹uùŸC>]€9’ (³Š;‰~ñÿŸ+I]ÓJ¼1Ú! +^Á¿m Yg�Gñ¿ÿÉЬ˜âÿ"´Zÿs¢Lk`yDN4 €£š×8±²ÈqŠ{ê=ÊjÑ´–¹é˜Q‡5ÙkØÅ‚›ËŠz.׶V T[ ÐÙàÿO7ÅÀ¢C‡ 5Í íà]Iðʸ]Qð +þCûÌ+€ãúÿ~ýOZHõÿËPŒi¬ è*%ŽÖ BVÄø§tƒ£¡ +©ÿÙªûg¿É65I§lÍ–0ûÐ…4F"T³çgÖÛ¶8¢6ø…O·T�õ;Ö�¯ø×…~D+ü‡ÁÀñÌ%úˆ€cø§…ƒüŸêÿ!l…˜ålâù8<Zo)°º/U¸eÝ[X·ýbsÔÔj–¡ùÏÝ™Qn/‡NÒ£¦nôÓ€?.%ø×¼*þùÓ ù÷÷ÿȇ4þç"<ä+ +röÐ$4=4Fvà+3è٦ݿÉÐ"#QÔþaäyŽGÊP²p´nÅw(›V¤å]o9¿-¶ºÃÅ]¾Þ/:Z2PxQ`»ülÚ—æû¿,%ø'ñ©Ÿ†–=¨ÿEó©þŠÔao±…Ü›L±ye˜ž8e¦˜c´tl´Óé6ô}} =S¬*óF+þ„[Yä! Ò^ÅLotLx<ÉÐ{fJõ6ÅW»%Ù`JÝƤXbÚÍ’4Õ–¦>º±ë8©ðøVZáß4ŒÏ›ÿ©ƒùŸæRýÿ"íÍ£|–i#_±Ô£iŸY7aýÀ´’¶÷¡Jw¡=†·ƒyW™V$…|݆RÖäNîî¹OYUbë~7MÔôÇ ÿQ°O›ÿêÿàù?Õÿ/Bвœ™bÚ>ÒB)¡k9Ô§‹çéuª-Nm–kWQÖR-‘¬$d€ùŒÝ«õweçæ8+QY;bÎ=6ÉœÔÏwä²gUVÍÖÊ“»Çåà‘ÊrÖó@Pø__8r ¥æ„sR‚sL’ÝôÏŸûÐQûßAþgNLíÿ—¡¡‹úÊ$L +ZŠüñÄíðÎÝßwÚƒ;T¤ìéÈy^Ô›ÞC¯4ïß5‡µæ´Íy¯‘ÂôÏ@;øÿ ÀQüsùÓü¢Àq¬‘‰µð¾~,ŸaèuïàÊÁ §Ë]×äš“Y¶0¤k¡Q@®f߇ƒ§ÇñC•áRèÿi(Á¿åôûXÃûœü¯x±ÿUHíÿ¡ÈŒ•þ)Ù6˜G=uCØCiÉL·^¾bN¨EAf MŸ‹ÌŒ^¶éš¬Ï˜–tû3P‚ÿ14OÓÿiá ÿ-¤öÿ‹ÐNÆãµIÞ¸ +a‰#ÂÀëõ1Ô¡Qgì>¬*/z1\”¹ý–Zv$AZ>V[ÖòÁI·õþ ´Á?‰” ž—¯ÿÈRùŸù´þËehŒ|?2Ò}ù’…8ƒü ƒHàlèùô™mÆÈTÐÏøÈsì蘇Ü-xÿþ¿~F5‘üûÿ×™ +ò4“Ÿ:‡HŠc|ÿüË®8’¯<›†WÅÒm¶?§J Á춥Y+Þ”+È5/tŒnû^J¥Éï þ‘^'Îѯÿ†gûÃúo©þJê¿jª$DJÒ'Ò€œÀ3¬!Ó²á7Ö}Ý%7hÇ/,: nöD•ú¥ªçª“Ѳ"‰þã|ózq(íYäï›pæƽwË—?3Ž"üug¦¼t¿<šöÈÇŠd±×I + âL<z§h'àË^%nÿ$ô§Ô;¨ÿȈ©þŠñæ̈$ÿ'Yçu Š:§k’™ÓýÿOÁ¿Ý[“éJNY^4Êm¥Û/?1ÓŽÞ +-íŽzbì¢ õìcìïqæ +ýYò…äøˆ\ò÷€¿}c=€þˆx1Œüé¯Ó6þ ÇÁÊà'¬ÿôž§Òü¡$þGDaüóŒLŽ†Pq`¡F©äxvߨÿûð/.<«Ë/å¥ÞgP.ÔrL©>y~hUÆlµšWP»E1[ÔâJògnR|™xR×Q’ÎcW�´ˆcÚ†ã#ù¢ÿ]ÚÆ?Ñõ>#þ‡ö÷ÿxšMý.B1þ]fÈJ�Òº†ç•ª¡ +€§xceå4þœøo0MÈ §ü4¢¶5œß?´ºh6ŠO¹¬ê.{…FÍì‘íHþ gn¥ö5íŒ §f?ø«ÀKB§à?FÛø'{ÁŸSÿù`þgSÿ¿ËPŒ¿‘ÒY@k2Æ?ÍP@bXð4ž1h†>+þsù +˜>ÑËe>ÞUiIŸd›J>,Õ†R¶x?mÏÅ܃¥H%mÏÿg®@-Lümõ‘E€t‹!ÿsÆ1dcÅAÇKu‘„$§¢`Ÿü»È‹VLŸaÿ§©ÿžgSûÿE(ªÿWî'ûc8ýv©+Íâù]¹ž£!ß_Çóô—¦›|1 (Å‘ž»õ ãú†øG_¾þzýÿũß\»ÿ[ß4~ºèõÃ'ømèÿfjÎoþÌøÍ°¦¿i}ó7âŽÖoºeý†æè7è»ÿüŸ_MR‰Í5+$É{×ÅûÁƒû·È‹-º£þúõ+é>ÆdÉðËÿ(Ôòí^ý6jø×/É_õý˜…þ…¯?Su‚LÑ mý—¯qã/_ã.ª£/pwú_[=ð·_ܵ( +‹D>±3vš™¿ý¹´¿efÐÏظ»AºgðR$˜ÑæÉ«Ø¿|uñàñ°_£‹ùr5<áQc&´PT-SÙnf^f¨£Ö*ƒ¶7Vmœm—U ŒüÄÈûOMj’×çóî`6ÌCî)uÝüq(‘ÿžÿQÅ:!þ‹9ðÿ Òø¯Ë6€¶¬Xjl/¨¾\a=jìÇacâ!J“ð©‰fÊ8 ˜g¾eG·m݉ÕêˆÜ1éq«cÌútžêM`ÉïäQ³äû†Ý° +Tä\Œü“½ÿOóÿâ¸Ãütªÿ]„VùßeÄʈ«AÀQ’ +$™1�^‰‹2K "䫸@w!ª¬}E¼Ã’}™ÿ@s8v-t9ã+ßrú0–«%G±Ñwò‰K>Eé˜o2_É^ìWüÓ¾Š«Ðâ1\¨‰ˆ‰£ÐfÈìH½o’lj„–Y´Wn%r¾A emÙ—jM³ŒìcóV1ǹEÒ^hÕ¬‰^–eÖç¯+üGůû–£ÂóûÃ?Ïìûó¼˜Îÿ!N±òeGYÛÌ1Š3£EzÔש_MÍs|Ç®I¯uÁúág>ùh“;*Ö B/·!MRÆÄYÕ\S#ÖÊÍÎA¬™†©%©Ö’«+\}ó6 ,Fœ›Ó/ñ}á {ÙW…"ªäs6&Ùþpii¦V|a²¾°¼…ÂrÚ¶ò9éO-íàÿ³â¿öë?Ò›Ú.CѬadò@FGà!X ž¯>Wä…¿P+ ¡º¼rz`6h=;ªõæzÌãÀSF£?5<þòD’\û(L»ÿQ&€ãø?ˆÿäSÿÏËÐ:ÃC8“Ä ²ûdú<ž±!7B3U›¹Ú«ÀçJÝw%WûªF±Õé8Ï÷s˺%žÒH°?.‘…Ôµa9ÎǤ~ˆè¨ýÛËÿL³¢˜ú]„°ú¯8¶µX-¢MWIÌ}<E¾ÌL[wf7–„hÕ½:[?%ÇK§gT–¨N·¤áÃX®·¦sC´U$üj°ªÆt[Õês®ÑõS‰q!Šðÿ¡ÑŸGñ/rüþ…´þëe()váÔè’]É›Ìßþ3J訛¾kÁ ¶¿ÿúùWûW;»Úï$Qñ¯3¶ +ÇÀXϨٙ1ÔQ™ÿ$Æå›h„k2BÏ ñ±Väg¯àØÉO°öTÏDÆÍ2µ^¥Æ©ÄË#ãxÍq¤Ú:Vü}²ŒÍ˜AÆ´ñÕE£ªž3#wBÎßS|N€Gÿ/ÒXµoo2d#7>`Ån£Ž±{Uf¼ë¡)ò|SµPt/íõµ ¹kzÈÇgÆ7€2:¾ArMø7rM3Ó²ð£q]ü`L#ºÄx@|Iz4�™íG• ÿ ¿?T‡Hˆo{r);×¹z~¯¼³½@¬²¾?s<]Á‹‚o}áÑ¢`šÕ8щ¿Ÿ,§OÙ9ß/~ü€¤,‘]yýž7¯ +?¿™óðUFѹ»äbȹ¢÷Er!’³c0òÈÌÈCQ…<Œè*VOã:SÖ¬��y§q×of†&ÂïI‹’1g¢Ýqz>•âj©¤ÚAl}÷ ˜:‰+¢pQ¸ ·ÀgÜ=b„dD|÷kþHæ:óWæƒèEï?Q®wÿý4ŸœøÌlŸwõ7Nš êŒX<ñÕ’tœääNã™ø¦£'ÿ«½:Ï yțӞ(gì½÷úöK|›¡lGÁüí9Sh)IâðSøª=€øÍ‘ÉV£Eo~W¤ÜóGåž„Ctàe¥ÿí¢ˆø’â\³Ï¹¹Ë)×µ’m†è[|ß®z–ªøÍ&W’Y]Éu¦FÞ±¹5ÇŇÐö+ñ†Hì$Ò¿BD–×Q)JhÚçm j¨:!óðú þ|È?GÜ—<׈G>î}dþ±¾y" +ÖàŸï¼¨xyÿuEO.zþHß{ö×ð!Ý"Îýà[‰½˜£R?Ó'¾ÞöRç• /æŠÕ€ùõÎgIªä €Ï“X™¬°¿a$5ñ.ÿF6Zk<{l”÷êÁ·ÞFr–Í]ÄúÅùnc5ÞßÇê4ÿü¦D#cîžmÖœ)ËÔn'a³WzTçlËÕ¥p2—-eyT§æ`¢çûE;Tdÿñ¬ÎâéÊ^Œðü»@Gý¿óÿBºÿsJâhžã ‘Cé€3tÈ:KIu¤C^äÑÉõ¿O‰ÿòËÇJM¿ÏMò”Z™¶ô`Àw:r½ûœÔW˜âóÝd`¸÷K'Éÿ±âÍu`vÕ¥óø’øˆ‘˜ÓOÖ!‰EÛEÞØô}¼ØŠ¼Q@æ–WÈ÷7ÅÁ?û|&má?įu1:ïfÐQüÓûø8>ÍÿqJêÿÒà<-�Šp£U€XÈ4%ª<O±èäú¿§à_¿§™Bµ5ä'ÂóÓK¶ä‡O÷¾M¤@oT®Ï¼NæzR¯ã›772`»uOЩ8‰"ü²ÿ‡xXÿ‹gÓýß‹Ðzf½ÉÄS*ºŠ¼0JìááÛvÜùrînmÂüv6VÍêõÞ•º´® +¶¯€¬¼a§^÷¾Õñ ×…)î±:~“™šX±p¼D‚¬û]%·?FÁÀÙ+CÝËÞîŠBÔr'ÄD8ŠÚõ/ñ³‰Nü êŠÇfƒZ…*wŸŸ*—ÂR丞F·µ+¾zjV•jƒÖøqôCʳ”RJ)¥”RJ)¥”RJ)¥”RJ)¥”RJiCÿÔÉÐ�h� \ No newline at end of file diff --git a/core/tests/fixtures/config_install/testing_config_install_no_config.tar.gz b/core/tests/fixtures/config_install/testing_config_install_no_config.tar.gz new file mode 100644 index 000000000000..06d740502001 --- /dev/null +++ b/core/tests/fixtures/config_install/testing_config_install_no_config.tar.gz @@ -0,0 +1 @@ +���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� \ No newline at end of file -- GitLab