ConfigConfigurator should be insensitive to key order when comparing active config to recipe config

Created on 22 January 2024, 9 months ago
Updated 13 February 2024, 8 months ago

Problem/Motivation

Right now, ConfigConfigurator does a simple strict equality comparison when comparing config that a recipe provides, to the same config as it exists in the database:

        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));
        }

The problem is that this will fail if the same exact keys and values exist in both versions, but in a different order.

Since these are both config arrays, we can't just do a recursive sort-by-key that ignores indexed arrays. That approach would suffice for many parts of config schema (in mappings, for example, order of the keys is functionally irrelevant), but for sequences, order may very well be important from the end user perspective. For example, CKEditor toolbar items are defined in an ordered sequence. So are user role permissions (which are not technically order-sensitive, but make for horrible, confusing diffs during config export if their order is just left to chance).

Proposed resolution

Generate a canonical representation of both the active config and recipe-defined config, by using config schema. Then perform the comparison.

The ability to generate a canonical representation is a bigger, longer-standing issue that will be fixed in core by πŸ“Œ Expose API to sort arbitrary config arrays Needs work . Once that's in, we can adopt the API it introduces. But, for the current purposes of the recipe system, a recursive sort-by-key is a "good enough" solution to make the recipe system a little more usable, so let's implement that.

Remaining tasks

Implement the change and add test coverage.

πŸ› Bug report
Status

Fixed

Version

10.2

Component

Code

Created by

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

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

Merge Requests

Comments & Activities

  • Issue created by @phenaproxima
  • Pipeline finished with Failed
    9 months ago
    #80883
  • Pipeline finished with Success
    9 months ago
    #80917
  • Status changed to Needs work 9 months ago
  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts
  • Pipeline finished with Failed
    9 months ago
    #80952
  • Pipeline finished with Failed
    9 months ago
    #80971
  • Status changed to Needs review 9 months ago
  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts
  • Pipeline finished with Failed
    9 months ago
    #80972
  • Pipeline finished with Running
    9 months ago
    #81104
  • Pipeline finished with Failed
    9 months ago
    #81110
  • Pipeline finished with Success
    9 months ago
    #81111
  • Status changed to Needs work 9 months ago
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    99% ready!

  • Pipeline finished with Pending
    9 months ago
    #81403
  • Pipeline finished with Canceled
    9 months ago
    #81402
  • Status changed to Needs review 9 months ago
  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts
  • Pipeline finished with Success
    9 months ago
    #81416
  • Status changed to Needs work 9 months ago
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί
  • Status changed to Needs review 9 months ago
  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts
  • Status changed to Needs work 9 months ago
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί
  • Pipeline finished with Running
    9 months ago
    #81530
  • Status changed to Needs review 9 months ago
  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts
  • Pipeline finished with Success
    9 months ago
    #81532
  • Status changed to RTBC 9 months ago
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    🚒

  • πŸ‡¨πŸ‡­Switzerland bircher πŸ‡¨πŸ‡Ώ

    I don't want to derail this, but the solution doesn't match the argument in the issue summary because to check if it is a mapping array_is_list is not enough.

    Also I would rather argue that doing the comparison at all is wrong, A recipe which adds a new role should not fail to apply when the role has gotten a new permission. But I haven't dug into the code enough in a sufficiently long time to not be in a position to make any claims... So I leave the issue status as is and just post a comment.

  • πŸ‡¬πŸ‡§United Kingdom alexpott πŸ‡ͺπŸ‡ΊπŸŒ

    A recipe which adds a new role should not fail to apply when the role has gotten a new permission

    This is why config actions exist.

    In general we've been working towards more ordered config over the last few years so I'm wondering where this has come up as a problem - it would be great to have specific examples. Such examples are missing from the issue summary.

  • Status changed to Needs work 9 months ago
  • πŸ‡¬πŸ‡§United Kingdom alexpott πŸ‡ͺπŸ‡ΊπŸŒ

    Note that I'm particularly against this change but it is a massive assumption that the order is not meaningless. It should not be. But when we first implemented it in core this was not the case. And @bircher is correct - you can only really sort maps by keys this way. @bircher does config split have some code we can copy here and should we be thinking about making this functionality a utility class or something outside of recipes for others to use?

    Setting to needs work for the array_is_list() function - and changing that to use config schema if available.

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

    I thought array_is_list() would be a reasonable way to detect whether something is a sequence or not β€” but you're right: type: sequence only means arbitrary keys are allowed, not that it's only integer-indexed keys (which is what https://www.php.net/manual/en/function.array-is-list.php checks).

    My bad!

    So, unfortunately this means that the logic here needs to become orders of magnitude more complex: the entire comparison must be schema-aware.

    @bircher Is there logic this could borrow from Config Split? πŸ™

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

    Per #16.

  • Status changed to Needs review 9 months ago
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    We need something like \Drupal\config_split\Config\ConfigSorter … or https://www.drupal.org/project/config_normalizer β†’ , which the code in config_split is recommending πŸ˜„

    IOW: we need to define how to build a canonical/normalized representation … because then we'd be able to do the comparison we need here.

    Unfortunately, both of those seem to be outdated β€” with last commits in 2021 and mostly saying "do X better when Y happens".

    So here's instead an initial stab at adding the necessary infrastructure, and … it was surprisingly simple? πŸ˜… Unless I overlooked something, which I probably did.

  • Pipeline finished with Failed
    9 months ago
    #82754
  • Pipeline finished with Failed
    9 months ago
    #82761
  • Pipeline finished with Failed
    9 months ago
    #82763
  • Pipeline finished with Success
    9 months ago
    #82765
  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts

    Been thinking on this and I think I finally see where @bircher, @alexpott, and Wim are coming from.

    Long story short - sequences are tricky. The approach I originally took here would detect "these two arrays contain different items, regardless of order" -- but it would not detect "these two arrays contain the same items, in a different order".

    Granted, in most situations, that probably would not matter too much, and the functional result would be identical, from the site builder's perspective. But the CKEditor toolbar is an excellent example of a place where the order of the items is exactly as important as what the items are.

    Having said that - my concern in this issue is more that it can make recipes unusable for people who may or may not have taken any conscious action to alter their config. So if getting past it is as relatively simple as Wim's initial work in the most recent commits, then I'm fully on board with that. But if it turns out to be a nest of dragons, my preference is to do a recursive-sort-by-key and then slay the dragons in other issues (which, to be clear, would need to be slain before the recipe system would be release-ready).

    I want to iterate quickly, especially since my current goals are about config validation with regard to recipes, which isn't really related to this problem. Given those circumstances, I'll take a quick, temporary workaround/fix over the long-slog, proper, dragonslayer fix for now.

  • Pipeline finished with Running
    9 months ago
    #82811
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    Addressed @bircher's feedback β€” I totally forgot about that for a bit πŸ˜‡ Was trivial to add 😊

    Looking further: I see no reason we cannot land this in Drupal core? πŸ˜„ I did add a new method to \Drupal\Core\Config\Schema\Element (which is an abstract class), so that qualifies as a API change/BC break. But there's ways around that (e.g. use an interface to make it optional instead of required).

  • Pipeline finished with Success
    9 months ago
    #82816
  • Status changed to Needs work 9 months ago
  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts

    I will update the IS here.

    I'm glad we landed on this solution. I think this fix is much more "correct", and feels more proper, than what I was proposing. I was quite worried by Wim's feeling in #16 that this would be incredibly complicated, because Wim and I have ping-ponged our way through very complex config schema issues and I'm still in therapy over them. I'm deeply relieved to see that it's not so bad at all!

    A few small points of review, and then I'd be happy RTBCing this if @alexpott and/or @bircher agree.

    I am +1 for getting it fixed in core as well.

  • Status changed to Needs review 9 months ago
  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts

    I think @bircher should provide final review here.

  • πŸ‡¬πŸ‡§United Kingdom alexpott πŸ‡ͺπŸ‡ΊπŸŒ

    Alternatively to all the complexity here can't we cast the incoming config - and if it matches all good :)

    We're going to need to somehow extract stuff from the Config and StorableConfigBase objects to do this... but that code probably should not be there anyway.

  • Pipeline finished with Canceled
    9 months ago
    Total: 88s
    #84043
  • Pipeline finished with Success
    9 months ago
    Total: 170s
    #84044
  • Pipeline finished with Success
    9 months ago
    Total: 169s
    #84046
  • Pipeline finished with Success
    9 months ago
    Total: 253s
    #84062
  • Pipeline finished with Success
    9 months ago
    Total: 200s
    #84084
  • Pipeline finished with Success
    9 months ago
    Total: 170s
    #84087
  • Pipeline finished with Success
    9 months ago
    Total: 133s
    #84089
  • Pipeline finished with Success
    9 months ago
    Total: 141s
    #84090
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί
    1. Alternatively to all the complexity here can't we cast the incoming config - and if it matches all good :)

      I don't see much complexity? πŸ˜…

      I think you're saying that an alternative could be to add a new public method that calls protected function castValue($key, $value) { … } on StorableConfigBase (and hence also on Config). But that is only available for simple config, not for config entities (which uses \Drupal\Core\Config\Entity\ConfigEntityStorage::mapToStorageRecord(), which in turn calls \Drupal\Core\Config\Entity\ConfigEntityBase::toArray(), which does NOT do any sorting).

      That's why I implemented this at the config schema/typed config level, so that it'd work for all config.

    2. I think you're looking at this and are thinking "hm, that's quite a bit of duplication of snippets of logic that already exist". But AFAICT this is doing it at the abstraction layer that A) makes it available for external use, B) moves the logic out of a single central place into the class associated with each type? Isn't this preferable?
    3. We're going to need to somehow extract stuff from the Config and StorableConfigBase objects to do this... but that code probably should not be there anyway.

      Ahhh! πŸ˜„ That's pretty much what I wrote above 😊

      Also, @bircher wrote in Slack:

      and finally the sorting in storable config should use this canonical representation when saving

      I tried to do what I think the two of you are looking for:

      1. use PrimitiveInterface::getCastedValue()
      2. match the "value" sorting logic from StorableConfigBase::castValue()
      3. match the "null" and "empty string" logic from StorableConfigBase::castValue()
      4. match the "do not drop mapping keys not defined in schema" logic from StorableConfigBase::castValue()
      5. Finally: Remove StorableConfigBase::castValue() in favor of Element::getCanonicalRepresentation().

      Result: the +306,-128 diffstat consists of +146,-23 test-only changes and +21,-3 recipe-logic-only changes.

      And … ::getCanonicalRepresentation() is used to save config. It behaves identically to StorableConfigBase::castValue(), but it's now a public API available via the Typed Data (i.e. schema-aware) representation of config.

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί
  • πŸ‡¨πŸ‡­Switzerland bircher πŸ‡¨πŸ‡Ώ

    Yes I think @alexpott in #23 is trying to say the same as me. And I also think you understood the idea.
    I don't have time this morning to properly look at it. But I think this should be in core!
    πŸ“Œ Expose API to sort arbitrary config arrays Needs work

    And yes RE #25

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

    From Slack:

    alexpott
    I feel like we need to move most of this to a core issue and land in there. FI we commit this to our repo it will make keeping inline with core way harder and we already know there are things out there that will benefit.

    alexpott
    Imo what we could do is open a core issue and work on that, and commit @phenaproxima’s original work with a massive @todo to use the core work.

    I completely agree with this.

    This MR, although absolutely the fix we want, now touches enough parts of core that it will be painful to chase HEAD with the recipe system patch. In the name of getting past the problem immediately facing recipe users, and the fact that the core issue will take longer to land, this feels like a solid approach to me.

    Tagging for a follow-up to either identify an existing core issue, or open a new one.

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

    @bircher identified πŸ“Œ Expose API to sort arbitrary config arrays Needs work as the core issue (already filed with a great summary!) where we should land the proper fix that Wim developed.

  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts
  • Pipeline finished with Success
    9 months ago
    Total: 171s
    #84400
  • Status changed to RTBC 9 months ago
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    Per #29.

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

    FYI: πŸ“Œ Expose API to sort arbitrary config arrays Needs work is now green and ready for review πŸ‘

  • πŸ‡¬πŸ‡§United Kingdom alexpott πŸ‡ͺπŸ‡ΊπŸŒ
  • Status changed to Fixed 9 months ago
    • e302e32f committed on patch
      Update recipe 10.2.x patch 86616f1b Issue #3416287 by phenaproxima, Wim...
    • 202a044e committed on patch
      Update recipe 11.x patch 0e1f31fc Issue #3416287 by phenaproxima, Wim...
  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.71.5 2024