Today submitting an unbatched `ClusterStateUpdateTask` to the `MasterService` is deprecated, and devs are encouraged to write their own batching executor instead. In principle this makes it possible to combine updates without incurring the costs of building a whole new `ClusterState` on each iteration, but in practice most implementations don't work at a lower level and just iteratively update a `ClusterState`. Moreover creating new `ClusterState` instances is nothing like as expensive as once it was.

We should accept this and provide some friendlier utilities for writing batched cluster state updates, ideally leaving the developer to express the desired per-task update as a `ClusterState -> ClusterState` operator.
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
* Return the name (id) of the highest matching index template for the given index name. In
     * the event that no templates are matched, {@code null} is returned.
     */
    @Nullable
    public static String findV2Template(Metadata metadata, String indexName, boolean isHidden) {
        final String resolvedIndexName = IndexNameExpressionResolver.DateMathExpressionResolver.resolveExpression(indexName);
        final Predicate<String> patternMatchPredicate = pattern -> Regex.simpleMatch(pattern, resolvedIndexName);
        final Map<ComposableIndexTemplate, String> matchedTemplates = new HashMap<>();
        for (Map.Entry<String, ComposableIndexTemplate> entry : metadata.templatesV2().entrySet()) {
            final String name = entry.getKey();
            final ComposableIndexTemplate template = entry.getValue();
            if (isHidden == false) {
                final boolean matched = template.indexPatterns().stream().anyMatch(patternMatchPredicate);
                if (matched) {
                    matchedTemplates.put(template, name);
                }
            } else {
                final boolean isNotMatchAllTemplate = template.indexPatterns().stream().noneMatch(Regex::isMatchAllPattern);
                if (isNotMatchAllTemplate) {
                    if (template.indexPatterns().stream().anyMatch(patternMatchPredicate)) {
                        matchedTemplates.put(template, name);
                    }
                }
            }
        }
        if (matchedTemplates.size() == 0) {
            return null;
        }
        final List<ComposableIndexTemplate> candidates = new ArrayList<>(matchedTemplates.keySet());
        CollectionUtil.timSort(candidates, Comparator.comparing(ComposableIndexTemplate::priorityOrZero, Comparator.reverseOrder()));