- π§πͺ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?
- πΊπΈ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.
- Status changed to Needs review
7 months ago 12:25pm 8 June 2024 - πΊπΈ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.
- ignore/skip: If I find the config, skip this
- apply/combine: If I find the config, add this
- replace/force: If I find the config, replace this
- 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, createI 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 thecreate
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 theensure_exists
andcreate
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 asensure_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 theconfig
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 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.
- πΊπΈUnited States thejimbirch Cape Cod, Massachusetts
Updated the documentation.
Automatically closed - issue fixed for 2 weeks with no activity.