Clarify recommended use case(s) for ensure_exists config action

Created on 26 August 2022, over 2 years ago
Updated 20 January 2023, almost 2 years ago

Problem/Motivation

The ensure_exists config action allows a config entity to be defined in an action rather than provided via a config .yml file. If an entity of the given type and ID already exists, no action is taken. Otherwise, the entity is created based on the definition given in the action.

Via ensure_exists, a given entity can be defined differently in multiple recipes. Since only the ID is tested for, the first recipe applied will "win". For example, a manager user role might be assigned is_admin: true on one recipe's config action and is_admin: false in another's. Whether the role has admin-level access will depend on which recipe is run first.

Because it produces potentially inconsistent results on a given site, it's not clear for which use case(s) this action would be a recommended solution.

Proposed resolution

Once we clarify what use cases the action meets, update documentation accordingly.

Remaining tasks

User interface changes

API changes

Data model changes

πŸ“Œ Task
Status

Active

Version

10.0

Component

Documentation

Created by

πŸ‡¨πŸ‡¦Canada nedjo

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

Merge Requests

Comments & Activities

Not all content is available!

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

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    Currently this would fail because the editor role recipe would run twice, and the second time it would try to add the role which already exists and has different permissions than originally.

    Isn't the problem here that

            if ($active_data !== $recipe_storage->read($config_name)) {
              throw new RecipePreExistingConfigException($config_name, sprintf("The configuration '%s' exists already and does not match the recipe's configuration", $config_name));
            }
    

    in \Drupal\Core\Recipe\ConfigConfigurator::__construct() is too simplistic?

    This will mean to not run recipes again that have already run and also decouple the direct dependency on it.

    Exactly!

    However, this would violate what seems to be a fundamental assumption in Recipes so far: there must be no trace left behind by the applying of a recipe, because if we want to not re-run the same recipe, then we'd need to track which recipes have in fact been applied πŸ˜…

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    #3 is a very basic use case and given the plan at #3304892: Determine which core module-provided config should be moved into recipes β†’ , the amount of composability that is intended to happen in core (and even more so in the real world) is going to hit this basically immediately.

    AFAICT this is a hard blocker that even a basic PoC (see #3304892-3: Determine which core module-provided config should be moved into recipes β†’ ) will run into. Increasing priority.

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    https://git.drupalcode.org/project/distributions_recipes/-/blob/1.0.x/do... will need to be updated with the findings of this issue.

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    Looks like is not entirely accurate, because >https://git.drupalcode.org/project/distributions_recipes/-/blob/1.0.x/do... says:

    • Do Drupal recipes have an installation status? The current opinion is that we should avoid using the word install with respect to a Drupal recipe. A Drupal recipe is something that can be applied against a site.
    • Should we have a log of what Drupal recipes have been applied to a site?
      Potential uses include:
      • Being able to use the applied Drupal recipe suggestions to recommend
      • further steps to take.
      • Being able to list Drupal recipes that can be reverted.
      • How is this different from an installation status?
      • Do we need to borrow the idea of the symfony.lock file from Symfony Flex?

    So it seems it's not yet fully decided?

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί
  • πŸ‡ΊπŸ‡ΈUnited States thejimbirch Cape Cod, Massachusetts

    Thansk to @wim Leers, we discovered that when you enable a theme, Drupal core's block_theme_initialize() makes the new theme get the same blocks as the current default theme (system.theme:default).

    The create a recipe that adds a theme, we needed to unset some blocks that are created by either of the default themes in Minimal (Stark) and Standard (Olivero), and for safeness, Claro should also be included as it is a core theme, and could be used on the front end for the default theme.

    Using the ensure_exists config action allows us to set these blocks to false, which basically treats them as optional and will not fail if those blocks aren't created by the default theme.

    Here is what that looks like:

    config:
      actions:
        block.block.gin_admin:
          ensure_exists:
            plugin: 'system_menu_block:admin'
          simple_config_update:
            status: false
    

    The complete array of blocks needed can be seen at https://github.com/kanopi/gin-admin-experience/blob/main/recipe.yml

    The downside of this is those blocks are created not enabled and adds some cruft to the site. They can be manually deleted.

    Is this a feature of core? Seems like it is a legacy solution. Shouldn't themes be able to provide their own blocks and not have to worry about which default theme is set.

  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts

    IMHO one possible solution here is for config actions to have a graceful failure mode that recipes can opt into. Something like "hey, I want to change this value on this block config...but if it doesn't exist, just skip over it quietly". That wouldn't be useful or desirable for every config entity type, but for blocks, it would be a godsend.

  • πŸ‡ΊπŸ‡ΈUnited States thejimbirch Cape Cod, Massachusetts

    I agree with that phenaproxima. How about:

    actions:
      block.block.gin_admin:
          optional_config_update:
            status: false
    

    Or would it need to be:

    optional_actions:
      block.block.gin_admin:
          simple_config_update:
            status: false
    

    I think either would be fine with recipe creators.

  • πŸ‡ΊπŸ‡ΈUnited States thejimbirch Cape Cod, Massachusetts

    I also have to back out the approach I took in #10 as it leads to different issues.

    I installed gin-admin-experience on minimal. For the blocks that were created from the non-startk themes, they aren't visible in the UI, so they can't be deleted, and cause config import issues:

    I am going to revert gin-admin-experience to only allow installation on the minimal profile.

  • πŸ‡ΊπŸ‡ΈUnited States thejimbirch Cape Cod, Massachusetts
  • πŸ‡ΊπŸ‡ΈUnited States thejimbirch Cape Cod, Massachusetts
  • Status changed to Needs review 7 months ago
  • πŸ‡ΊπŸ‡ΈUnited States thejimbirch Cape Cod, Massachusetts

    We need a decision point in ensure_exists, or similar actions that offer different paths.

    The decision point is what to do at the point where the recipe runner finds or doesn't find a config while applying an action.

    1. ignore/skip: If I find the config, skip this
    2. apply/combine: If I find the config, add this
    3. replace/force: If I find the config, replace this
    4. create: If I don't find the config, do this

    config:
    actions:
    example.machine_name
    ensure_exists:
    id: machine_name
    directive: ignore/skip, apply/combine, replace/force, create

    I feel like this would greatly increase the interoperability of recipe application, especially when harnessed with the little used create config action.

    The following is a rewrite of core's Remote video media recipe. Instead of having a /config folder where config files live, we are using the create action to have it all in a single recipe file.

    name: 'Remote video media'
    description: 'Provides a media type for videos hosted on YouTube and Vimeo.'
    type: 'Media type'
    install:
      - image
      - media
      - media_library
      - path
      - views
    config:
      import:
        media_library:
          - core.entity_view_mode.media.media_library
          - core.entity_form_mode.media.media_library
          - image.style.media_library
          - views.view.media_library
        media:
          - core.entity_view_mode.media.full
          - system.action.media_delete_action
          - system.action.media_publish_action
          - system.action.media_save_action
          - system.action.media_unpublish_action
          - views.view.media
        image:
          - image.style.medium
      actions:
        media.type.remote_video:
          create:
            langcode: en
            status: true
            dependencies: {  }
            id: remote_video
            label: 'Remote video'
            description: 'A remotely hosted video from YouTube or Vimeo.'
            source: 'oembed:video'
            queue_thumbnail_downloads: false
            new_revision: true
            source_configuration:
              source_field: field_media_oembed_video
              thumbnails_directory: 'public://oembed_thumbnails/[date:custom:Y-m]'
              providers:
                - YouTube
                - Vimeo
            field_map:
              title: name
        field.storage.media.field_media_oembed_video:
          create:
            langcode: en
            status: true
            dependencies:
              module:
                - media
            id: media.field_media_oembed_video
            field_name: field_media_oembed_video
            entity_type: media
            type: string
            settings:
              max_length: 255
              case_sensitive: false
              is_ascii: false
            module: core
            locked: false
            cardinality: 1
            translatable: true
            indexes: {  }
            persist_with_no_fields: false
            custom_storage: false
        field.field.media.remote_video.field_media_oembed_video:
          create:
            langcode: en
            status: true
            dependencies:
              config:
                - field.storage.media.field_media_oembed_video
                - media.type.remote_video
            id: media.remote_video.field_media_oembed_video
            field_name: field_media_oembed_video
            entity_type: media
            bundle: remote_video
            label: 'Video URL'
            description: ''
            required: true
            translatable: true
            default_value: {  }
            default_value_callback: ''
            settings: {  }
            field_type: string
        core.entity_form_display.media.remote_video.default:
          create:
            langcode: en
            status: true
            dependencies:
              config:
                - field.field.media.remote_video.field_media_oembed_video
                - media.type.remote_video
              module:
                - media
                - path
            id: media.remote_video.default
            targetEntityType: media
            bundle: remote_video
            mode: default
            content:
              created:
                type: datetime_timestamp
                weight: 10
                region: content
                settings: {  }
                third_party_settings: {  }
              field_media_oembed_video:
                type: oembed_textfield
                weight: 0
                region: content
                settings:
                  size: 60
                  placeholder: ''
                third_party_settings: {  }
              path:
                type: path
                weight: 30
                region: content
                settings: {  }
                third_party_settings: {  }
              status:
                type: boolean_checkbox
                weight: 100
                region: content
                settings:
                  display_label: true
                third_party_settings: {  }
              uid:
                type: entity_reference_autocomplete
                weight: 5
                region: content
                settings:
                  match_operator: CONTAINS
                  match_limit: 10
                  size: 60
                  placeholder: ''
                third_party_settings: {  }
            hidden:
              name: true
        core.entity_form_display.media.remote_video.media_library:
          create:
            langcode: en
            status: true
            dependencies:
              config:
                - core.entity_form_mode.media.media_library
                - field.field.media.remote_video.field_media_oembed_video
                - media.type.remote_video
            id: media.remote_video.media_library
            targetEntityType: media
            bundle: remote_video
            mode: media_library
            content: {  }
            hidden:
              created: true
              field_media_oembed_video: true
              name: true
              path: true
              status: true
              uid: true
        core.entity_view_display.media.remote_video.default:
          create:
            langcode: en
            status: true
            dependencies:
              config:
                - field.field.media.remote_video.field_media_oembed_video
                - media.type.remote_video
              module:
                - media
            id: media.remote_video.default
            targetEntityType: media
            bundle: remote_video
            mode: default
            content:
              field_media_oembed_video:
                type: oembed
                label: hidden
                settings:
                  max_width: 0
                  max_height: 0
                  loading:
                    attribute: lazy
                third_party_settings: {  }
                weight: 0
                region: content
            hidden:
              created: true
              name: true
              thumbnail: true
              uid: true
        core.entity_view_display.media.remote_video.media_library:
          create:
            langcode: en
            status: true
            dependencies:
              config:
                - core.entity_view_mode.media.media_library
                - field.field.media.remote_video.field_media_oembed_video
                - image.style.medium
                - media.type.remote_video
              module:
                - image
            id: media.remote_video.media_library
            targetEntityType: media
            bundle: remote_video
            mode: media_library
            content:
              thumbnail:
                type: image
                label: hidden
                settings:
                  image_style: medium
                  image_link: ''
                  image_loading:
                    attribute: lazy
                third_party_settings: {  }
                weight: 0
                region: content
            hidden:
              created: true
              field_media_oembed_video: true
              name: true
              uid: true
    

    Note: you will have to clear the cache after applying this recipe to avoid a WSOD.

    With the proposed changes, the action to skip adding the media type if it already exists would be:

      actions:
        media.type.remote_video:
          ensure_exists:
            id: remote_video
            directive: skip
          create:
            langcode: en
            status: true
            dependencies: {  }
            id: remote_video
    

    This would allow the recipe to continue to apply if it encounters it and it is not exactly what is expected. This action could be applied to all of the fields and displays in the recipe allowing for granular decisions if needed by the recipe.

    If this proposal is accepted, moving forward, I feel we need to:

    1. Extend the ensure_exists action to have the directive option, or add additional actions.
    2. Update the core recipes as needed to use the options as needed.
    3. Update the documentation around the ensure_exists and create actions.
    4. Review the issue queue, especially the Improve the recipe application process β†’ section of the phase 3 roadmap to see which of those issues could be closed if they updated their recipes to this workflow.

  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts

    This is a documentation issue first and foremost.

    If your recipe needs a particular config entity to exist (by ID), but does not care about what it contains, use createIfNotExists (formerly known as ensure_exists). This way is best for things like user roles, where you most likely don't care about its label or weight, but probably just want to add some permissions (which you can do with a config action). Another good case for this is, say, entity view displays, where the label isn't very important but the arrangement of components -- again, something you can change with config actions -- is.

    If the recipe needs to create a config entity and requires that it look a particular way (even if that's only one or two values), putting it in the config directory is the way forward. Or, you can modify an existing one with config actions. This way is best for things like fields, which generally must have specific configurations that affect the way data is stored.

    To put it another way, createIfNotExists says "if Drupal already has a config entity with this ID, I'm going to use that". Everything in the config directory, on the other hand, is saying "if Drupal already has a config entity with this ID, it had better be exactly the same as the one I'm providing."

    Both ways guarantee that the entity will exist once the recipe is done. But the first way is more lenient, and the second way is stricter. Generally speaking, strict is better, because it's predictable. Strictness is less flexible, but you know what you're getting. But flexibility can be useful too, which is why createIfNotExists is there. But beware of too much flexibility -- that leads to unpredictability, and maintenance nightmares arising from that.

  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts
  • πŸ‡ΊπŸ‡ΈUnited States thejimbirch Cape Cod, Massachusetts

    With the introduction of config:strict β†’ in 10.4.0+, there is a way to allow leniency in config import that is much safer to use than createIfNotExists

    config:
      # The default.
      strict: true 
      # If any config exists, skip them.
      strict: false
      # Ensure some configs are the same. 
      # All others are false.
      strict:
        - field.storage.foo
        - taxonomy.vocabulary.bar	
    

    Updating the documentation based on this.

  • Merge request !170Adds clarity to createIfNotExists β†’ (Merged) created by thejimbirch
  • πŸ‡ΊπŸ‡ΈUnited States thejimbirch Cape Cod, Massachusetts

    Updated the documentation.

  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.71.5 2024