Reverting an Overridden Config using Config Update Writes the Wrong Config into Config Storage

Created on 19 July 2022, over 2 years ago
Updated 5 March 2024, 10 months ago

Problem/Motivation

If existing Module B starts overriding Config X from Module A, declares an implementation of hook_install_config_overrides(), and uses Configuration Update Manager in an update hook to revert the site's copy of Config X to match the copy that Module B provides, the copy of Config X from Module A gets deployed instead of the copy from Module B.

Steps to reproduce

  1. Install "Configuration Update Manager" and "Install-time Config Override" on a Drupal site.
  2. Create a new module called "Module B" (module_b). In other words, create a folder called module_b that contains a module_b.info.yml file with the minimum required metadata for Drupal 9 to recognize it.
  3. Enable Module B on the site.
  4. Inside Module B, create the file config/install/dblog.settings.yml that contains the following code:
    row_limit: 100000
    
  5. Add the following to the module_b.install file:
    function module_b_update_9000(): void {
      $update_service = \Drupal::service('config_update.config_update');
      assert($update_service instanceof ConfigRevertInterface);
    
      $update_service->import('system.simple', 'dblog.settings');
    }
    
    function module_b_install_config_overrides(): array {
      return [
        'dblog.settings',
      ];
    }
    
  6. Declare install_config_overrides:install_config_overrides as a dependency in the module_b.info.yml file.
  7. Run database updates so that module_b_update_9000 runs.
  8. Visit /admin/config/development/logging and check the value of the "Database log messages to keep" setting.

Expected Results

The setting should indicate 100,000 records to keep, demonstrating that the setting was sourced from the override.

Actual Results

The setting indicates 1000 records to keep, indicating that the core copy of the file was loaded.

πŸ› Bug report
Status

Active

Version

1.0

Component

Code

Created by

πŸ‡ΊπŸ‡ΈUnited States RyanCMcConnell

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡ΊπŸ‡ΈUnited States GuyPaddock

    I rewrote the description of this issue because I wasn't 100% sure what the original description was alluding to and I just ran into this issue when reverting a config as part of an existing module that uses install-time config overrides.

  • πŸ‡ΊπŸ‡ΈUnited States GuyPaddock
  • πŸ‡ΊπŸ‡ΈUnited States GuyPaddock
  • πŸ‡ΊπŸ‡ΈUnited States GuyPaddock

    I am not sure if a lot can be done inside the ICO module to handle this case better, since it doesn't impact how configuration files get discovered by Core. It appears that Core caches only a single path for each config, so solving this problem might require a custom InstallStorage implementation that changes the way that getAllFolders() builds its cache under the hood.

    As a workaround, an update hook could write the desired configuration directly into configuration storage using a utility method like this:

    /**
     * Import a config from an explicit file path into database storage.
     *
     * @param string $config_name
     *   The name of the config item to import from the config folder.
     * @param string $config_path
     *   The path to the config folder.
     *
     * @return bool
     *   TRUE on success, or FALSE if the config could not be installed.
     */
    function my_module_import_single_config(string $config_name, string $config_path): bool {
      $source = new FileStorage($config_path);
    
      $config_storage = \Drupal::service('config.storage');
      assert($config_storage instanceof StorageInterface);
    
      return $config_storage->write($config_name, $source->read($config_name));
    }
    

    This is risky, since the config does NOT get validated. A safer version could look like this:

    /**
     * Import a config from an explicit file path by its entity type and ID.
     *
     * @param string $config_path
     *   The path to the config folder.
     * @param string $entity_type
     *   The machine name of the configuration entity being imported.
     * @param string $id
     *   The ID of the configuration being imported.
     */
    function my_module_import_single_config_safer(string $config_path, string $entity_type, string $id): void {
      $source = new FileStorage($config_path);
    
      try {
        $config_full_name = _arkayo_am_get_full_config_name($entity_type, $id);
        $config_storage   = \Drupal::entityTypeManager()->getStorage($entity_type);
    
        // _arkayo_am_get_full_config_name() should have thrown an exception on the
        // entity type by now if this assertion does not hold.
        assert($config_storage instanceof ConfigEntityStorageInterface);
    
        $config_data = $source->read($config_full_name);
    
        $entity = $config_storage->createFromStorageRecord($config_data);
        $entity->save();
      }
      catch (PluginNotFoundException | InvalidPluginDefinitionException $ex) {
        throw new InvalidArgumentException(
          sprintf(
            'Invalid configuration entity type ("%s"): %s',
            $entity_type,
            $ex->getMessage()
          )
        );
      }
      catch (EntityStorageException $ex) {
        throw new InvalidArgumentException(
          sprintf(
            'Failed to import configuration to "%s": %s',
            $config_full_name ?? sprintf('%s.%s', $entity_type, $id),
            $ex->getMessage()
          ));
      }
    }
    
Production build 0.71.5 2024