Problem/Motivation

This is best described by referring to this documentation clarification issue I just opened: #3560169: Clarify that "config.import.mymodule: *" imports all optional configuration without checking dependencies

The current documentation states:

For a given module or theme, you have two options. First, you can use a * wildcard to install all configuration the module provides, just as would happen when the extension is installed "normally" (not via a recipe).

This is not accurate, because using a * wildcard will install ALL of the module's configuration, including entities in config/optional and it will NOT filter out optional configuration that has unmet dependencies, which is how it "normally" works.

Steps to reproduce

Attempt to apply a recipe that installs a module with optional configuration that has unmet dependencies, using the config.import.[module].* wildcard syntax.

Proposed resolution

Can we make this work the same as it "normally" works when installing extensions? Can it install all of the module's configuration, and filter out optional config with missing dependencies?

Remaining tasks

TBD

User interface changes

None.

Introduced terminology

None.

API changes

None.

Data model changes

None.

Release notes snippet

None.

Comments

m.stenta created an issue. See original summary.

m.stenta’s picture

phenaproxima’s picture

This has been asked for many times, and I have generally pushed back on it. @alexpott may also be able to provide clarification.

This is a tricky area because the way optional configuration works goes directly against the intrinsic goals and guardrails of a recipe.

A recipe needs to be, above all, predictable. Recipes can be applied to a site at any point in its lifecycle, and be composed into huge, complicated clusters and stacks. Both of these things mean that a recipe has to be predictable. You need to know exactly what it will do, just by reading it. Recipes must, in every respect, have a guaranteed outcome.

But, from a recipe's perspective, optional config is unpredictable. That's not to say optional config is random; it does operate by a known and clear set of rules. Nonetheless, exactly what optional config you will get really depends on the state of the system at any given point in time. What extensions -- including the install profile -- are installed? What config entities exist? Those can change over time, and whenever they do, it directly changes what optional config can be installed from that point on. Recipes have no way to know the state of the system, and alter themselves to fit it. If they did, that would violate one of the main principles of recipes, which is that they are dumb and cannot contain logic or make decisions.

In other words, if recipes installed optional config the "usual" way, applying a single recipe could produce different results depending on when you applied it. That harms, if not outright breaks, both predictability and composability. How can I build a recipe on top of another recipe which can produce different outcomes depending on factors which I, as the recipe author, have no way to know in advance?

If recipes' predictability and composability is harmed, a great deal of recipes' value is lost. That's why I am generally against the idea of having recipes be in the optional config business as we know it. And it's why I suspect the recipe system, as created by @alexpott, was designed this way.

Doing this would also open the door to an entirely new class of bugs which simply doesn't exist right now. And it would increase the complexity of the recipe system (which is currently fairly simple) by orders of magnitude, since now we have to deal with dynamic dependency resolution, which is problematic in the best of times.

Now, having said all that, I can see why the choice between either "import everything" (*) and "import some stuff" (an array of config names) is limiting. One thing I was considering was maybe introducing support for a new symbol (this is just a guess at syntax):

config:
  import:
    foo_module: '!'

This would mean "import all config from foo_module, except for its optional config".

To me, this:

  • Improves DX, since you don't have to list individual entities if you just want the module's default config.
  • Draws a clear distinction between optional and required config.
  • Continues to give recipe authors the option to also import optional config (the behavior of * would be unchanged).
  • Keeps recipes predictable, since you know what you're getting. Obviously you might get different config if foo_module changes the config it ships with, but that is a dependency versioning issue and does not make the recipe more sensitive to the state of the system.
m.stenta’s picture

Thanks for taking the time to reply so thoroughly @phenaproxima. I am coming to recipes late(r), so there's a lot to catch up on.

Everything you said makes sense. I will have to consider all this, and whether or not recipes are a workable solution for our use-case (described here: #3560179: Support installing all module configuration entities via recipes).

@bryan also pointed to this as one of the original discussions: #3393086: Optional configuration is imported even when module dependencies aren't there.

So I will close this as a duplicate. Thanks again for the explanations.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.