Browse Source

IndexNameExpressionResolver refactoring (#116085) (#117267)

* Refactor DateMathExpressionResolver.

In this commit we reduce the scope of the DateMathExpressionResolver to only handle one expression at a time. This simplifies the code since it move the preprocessing from the date math calculation.

Furthermore, we simplify the API, so it does not need a context.

Finally, the reduced scope allowed us to reduce the test footprint. The tests are targeted only to the single expression date math resolution and any test with expression combinations will be moved to the IndexNameExpressionResolverTests.

* Create SystemResourceAccess.

In this class we collect all the related access checks to system indices.

These checks are not straight forward and there are different rules that apply on different parts of the code.

In this PR, we just collect them in one place to allow further analysis to determine if these differences are a feature or a bug.

* Refactor WildcardExpressionResolver.

In this PR we reduced the scope of the WildcardExpressionResolver to resolve one expression at a time. It also still supports the `*`.

This allows us to reduce the scope of the test as well.

Furthermore, we switched the usage of streams to more imperative code to reduce the object creation.

* Refactor expression resolution to resources.

In this PR we bring all the previous steps together. We change the expression resolution, instead of processing lists of expressions to completely resolve one expression to its resources before moving to the next.

This intends to increase the maintainability of the code, because we can debug it easier and we reduce the code duplication when dealing with exclusions and other pre-processing tasks.

* Fix format

* Bug fix: do the empty check on wildcard expressions on each wildcard

* Polishing

* Optimise for no wildcards

* Fix test name typo

* Replace for-each loops with for-i loops

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: James Baiera <james.baiera@gmail.com>
(cherry picked from commit 7dc2cc6b7f68ca7bbdb138fc239dda8204df4556)
Mary Gouseti 10 months ago
parent
commit
4aaa09c50c

+ 446 - 389
server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java

@@ -48,17 +48,24 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.SortedMap;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.LongSupplier;
 import java.util.function.Predicate;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
+/**
+ * This class main focus is to resolve multi-syntax target expressions to resources or concrete indices. This resolution is influenced
+ * by IndicesOptions and other flags passed through the method call. Examples of the functionality it provides:
+ * - Resolve expressions to concrete indices
+ * - Resolve expressions to data stream names
+ * - Resolve expressions to resources (meaning indices, data streams and aliases)
+ * Note: This class is performance sensitive, so we pay extra attention on the data structure usage and we avoid streams and iterators
+ * when possible in favor of the classic for-i loops.
+ */
 public class IndexNameExpressionResolver {
     private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(IndexNameExpressionResolver.class);
 
@@ -190,7 +197,7 @@ public class IndexNameExpressionResolver {
             getSystemIndexAccessPredicate(),
             getNetNewSystemIndexPredicate()
         );
-        final Collection<String> expressions = resolveExpressions(context, indexExpressions);
+        final Collection<String> expressions = resolveExpressionsToResources(context, indexExpressions);
         return expressions.stream()
             .map(x -> state.metadata().getIndicesLookup().get(x))
             .filter(Objects::nonNull)
@@ -220,7 +227,7 @@ public class IndexNameExpressionResolver {
             getNetNewSystemIndexPredicate()
         );
 
-        final Collection<String> expressions = resolveExpressions(context, request.index());
+        final Collection<String> expressions = resolveExpressionsToResources(context, request.index());
 
         if (expressions.size() == 1) {
             IndexAbstraction ia = state.metadata().getIndicesLookup().get(expressions.iterator().next());
@@ -236,7 +243,7 @@ public class IndexNameExpressionResolver {
                     );
                 }
             }
-            checkSystemIndexAccess(context, Set.of(ia.getWriteIndex()));
+            SystemResourceAccess.checkSystemIndexAccess(context, threadContext, ia.getWriteIndex());
             return ia;
         } else {
             throw new IllegalArgumentException(
@@ -245,30 +252,110 @@ public class IndexNameExpressionResolver {
         }
     }
 
-    protected static Collection<String> resolveExpressions(Context context, String... expressions) {
-        if (context.getOptions().expandWildcardExpressions() == false) {
+    /**
+     * Resolve the expression to the set of indices, aliases, and, optionally, data streams that the expression matches.
+     * If {@param preserveDataStreams} is {@code true}, data streams that are covered by the wildcards from the
+     * {@param expressions} are returned as-is, without expanding them further to their respective backing indices.
+     */
+    protected static Collection<String> resolveExpressionsToResources(Context context, String... expressions) {
+        // If we do not expand wildcards, then empty or _all expression result in an empty list
+        boolean expandWildcards = context.getOptions().expandWildcardExpressions();
+        if (expandWildcards == false) {
             if (expressions == null || expressions.length == 0 || expressions.length == 1 && Metadata.ALL.equals(expressions[0])) {
                 return List.of();
-            } else {
-                return ExplicitResourceNameFilter.filterUnavailable(
-                    context,
-                    DateMathExpressionResolver.resolve(context, Arrays.asList(expressions))
-                );
             }
         } else {
             if (expressions == null
                 || expressions.length == 0
                 || expressions.length == 1 && (Metadata.ALL.equals(expressions[0]) || Regex.isMatchAllPattern(expressions[0]))) {
                 return WildcardExpressionResolver.resolveAll(context);
+            } else if (isNoneExpression(expressions)) {
+                return List.of();
+            }
+        }
+
+        // Using ArrayList when we know we do not have wildcards is an optimisation, given that one expression result in 0 or 1 resources.
+        Collection<String> resources = expandWildcards && WildcardExpressionResolver.hasWildcards(expressions)
+            ? new LinkedHashSet<>()
+            : new ArrayList<>(expressions.length);
+        boolean wildcardSeen = false;
+        for (int i = 0, n = expressions.length; i < n; i++) {
+            String originalExpression = expressions[i];
+
+            // Resolve exclusion, a `-` prefixed expression is an exclusion only if it succeeds a wildcard.
+            boolean isExclusion = wildcardSeen && originalExpression.startsWith("-");
+            String baseExpression = isExclusion ? originalExpression.substring(1) : originalExpression;
+
+            // Resolve date math
+            baseExpression = DateMathExpressionResolver.resolveExpression(baseExpression, context::getStartTime);
+
+            // Validate base expression
+            validateResourceExpression(context, baseExpression, expressions);
+
+            // Check if it's wildcard
+            boolean isWildcard = expandWildcards && WildcardExpressionResolver.isWildcard(originalExpression);
+            wildcardSeen |= isWildcard;
+
+            if (isWildcard) {
+                Set<String> matchingResources = WildcardExpressionResolver.matchWildcardToResources(context, baseExpression);
+
+                if (context.getOptions().allowNoIndices() == false && matchingResources.isEmpty()) {
+                    throw notFoundException(baseExpression);
+                }
+
+                if (isExclusion) {
+                    resources.removeAll(matchingResources);
+                } else {
+                    resources.addAll(matchingResources);
+                }
             } else {
-                return WildcardExpressionResolver.resolve(
-                    context,
-                    ExplicitResourceNameFilter.filterUnavailable(
-                        context,
-                        DateMathExpressionResolver.resolve(context, Arrays.asList(expressions))
-                    )
-                );
+                if (isExclusion) {
+                    resources.remove(baseExpression);
+                } else if (ensureAliasOrIndexExists(context, baseExpression)) {
+                    resources.add(baseExpression);
+                }
+            }
+        }
+        return resources;
+    }
+
+    /**
+     * Validates the requested expression by performing the following checks:
+     * - Ensure it's not empty
+     * - Ensure it doesn't start with `_`
+     * - Ensure it's not a remote expression unless the allow unavailable targets is enabled.
+     */
+    private static void validateResourceExpression(Context context, String current, String[] expressions) {
+        if (Strings.isEmpty(current)) {
+            throw notFoundException(current);
+        }
+        // Expressions can not start with an underscore. This is reserved for APIs. If the check gets here, the API
+        // does not exist and the path is interpreted as an expression. If the expression begins with an underscore,
+        // throw a specific error that is different from the [[IndexNotFoundException]], which is typically thrown
+        // if the expression can't be found.
+        if (current.charAt(0) == '_') {
+            throw new InvalidIndexNameException(current, "must not start with '_'.");
+        }
+        ensureRemoteExpressionRequireIgnoreUnavailable(context.getOptions(), current, expressions);
+    }
+
+    /**
+     * Throws an exception if the expression is a remote expression and we do not allow unavailable targets
+     */
+    private static void ensureRemoteExpressionRequireIgnoreUnavailable(IndicesOptions options, String current, String[] expressions) {
+        if (options.ignoreUnavailable()) {
+            return;
+        }
+        if (RemoteClusterAware.isRemoteIndexName(current)) {
+            List<String> crossClusterIndices = new ArrayList<>();
+            for (int i = 0; i < expressions.length; i++) {
+                if (RemoteClusterAware.isRemoteIndexName(expressions[i])) {
+                    crossClusterIndices.add(expressions[i]);
+                }
             }
+            throw new IllegalArgumentException(
+                "Cross-cluster calls are not supported in this context but remote indices were requested: " + crossClusterIndices
+            );
         }
     }
 
@@ -341,7 +428,7 @@ public class IndexNameExpressionResolver {
     }
 
     Index[] concreteIndices(Context context, String... indexExpressions) {
-        final Collection<String> expressions = resolveExpressions(context, indexExpressions);
+        final Collection<String> expressions = resolveExpressionsToResources(context, indexExpressions);
 
         final Set<Index> concreteIndicesResult = Sets.newLinkedHashSetWithExpectedSize(expressions.size());
         final Map<String, IndexAbstraction> indicesLookup = context.getState().metadata().getIndicesLookup();
@@ -395,7 +482,9 @@ public class IndexNameExpressionResolver {
                     && context.getOptions().includeFailureIndices()) {
                         // Collect the data streams involved
                         Set<DataStream> aliasDataStreams = new HashSet<>();
-                        for (Index index : indexAbstraction.getIndices()) {
+                        List<Index> indices = indexAbstraction.getIndices();
+                        for (int i = 0, n = indices.size(); i < n; i++) {
+                            Index index = indices.get(i);
                             aliasDataStreams.add(indicesLookup.get(index.getName()).getParentDataStream());
                         }
                         for (DataStream dataStream : aliasDataStreams) {
@@ -416,13 +505,16 @@ public class IndexNameExpressionResolver {
         if (context.getOptions().allowNoIndices() == false && concreteIndicesResult.isEmpty()) {
             throw notFoundException(indexExpressions);
         }
-        checkSystemIndexAccess(context, concreteIndicesResult);
-        return concreteIndicesResult.toArray(Index.EMPTY_ARRAY);
+        Index[] resultArray = concreteIndicesResult.toArray(Index.EMPTY_ARRAY);
+        SystemResourceAccess.checkSystemIndexAccess(context, threadContext, resultArray);
+        return resultArray;
     }
 
     private static void resolveIndicesForDataStream(Context context, DataStream dataStream, Set<Index> concreteIndicesResult) {
         if (shouldIncludeRegularIndices(context.getOptions())) {
-            for (Index index : dataStream.getIndices()) {
+            List<Index> indices = dataStream.getIndices();
+            for (int i = 0, n = indices.size(); i < n; i++) {
+                Index index = indices.get(i);
                 if (shouldTrackConcreteIndex(context, index)) {
                     concreteIndicesResult.add(index);
                 }
@@ -431,7 +523,9 @@ public class IndexNameExpressionResolver {
         if (shouldIncludeFailureIndices(context.getOptions())) {
             // We short-circuit here, if failure indices are not allowed and they can be skipped
             if (context.getOptions().allowFailureIndices() || context.getOptions().ignoreUnavailable() == false) {
-                for (Index index : dataStream.getFailureIndices().getIndices()) {
+                List<Index> failureIndices = dataStream.getFailureIndices().getIndices();
+                for (int i = 0, n = failureIndices.size(); i < n; i++) {
+                    Index index = failureIndices.get(i);
                     if (shouldTrackConcreteIndex(context, index)) {
                         concreteIndicesResult.add(index);
                     }
@@ -482,64 +576,6 @@ public class IndexNameExpressionResolver {
         return indexAbstraction.getIndices().size() > 1;
     }
 
-    private void checkSystemIndexAccess(Context context, Set<Index> concreteIndices) {
-        final Predicate<String> systemIndexAccessPredicate = context.getSystemIndexAccessPredicate();
-        if (systemIndexAccessPredicate == Predicates.<String>always()) {
-            return;
-        }
-        doCheckSystemIndexAccess(context, concreteIndices, systemIndexAccessPredicate);
-    }
-
-    private void doCheckSystemIndexAccess(Context context, Set<Index> concreteIndices, Predicate<String> systemIndexAccessPredicate) {
-        final Metadata metadata = context.getState().metadata();
-        final List<String> resolvedSystemIndices = new ArrayList<>();
-        final List<String> resolvedNetNewSystemIndices = new ArrayList<>();
-        final Set<String> resolvedSystemDataStreams = new HashSet<>();
-        final SortedMap<String, IndexAbstraction> indicesLookup = metadata.getIndicesLookup();
-        boolean matchedIndex = false;
-        for (Index concreteIndex : concreteIndices) {
-            IndexMetadata idxMetadata = metadata.index(concreteIndex);
-            String name = concreteIndex.getName();
-            if (idxMetadata.isSystem() && systemIndexAccessPredicate.test(name) == false) {
-                matchedIndex = true;
-                IndexAbstraction indexAbstraction = indicesLookup.get(name);
-                if (indexAbstraction.getParentDataStream() != null) {
-                    resolvedSystemDataStreams.add(indexAbstraction.getParentDataStream().getName());
-                } else if (systemIndices.isNetNewSystemIndex(name)) {
-                    resolvedNetNewSystemIndices.add(name);
-                } else {
-                    resolvedSystemIndices.add(name);
-                }
-            }
-        }
-        if (matchedIndex) {
-            handleMatchedSystemIndices(resolvedSystemIndices, resolvedSystemDataStreams, resolvedNetNewSystemIndices);
-        }
-    }
-
-    private void handleMatchedSystemIndices(
-        List<String> resolvedSystemIndices,
-        Set<String> resolvedSystemDataStreams,
-        List<String> resolvedNetNewSystemIndices
-    ) {
-        if (resolvedSystemIndices.isEmpty() == false) {
-            Collections.sort(resolvedSystemIndices);
-            deprecationLogger.warn(
-                DeprecationCategory.API,
-                "open_system_index_access",
-                "this request accesses system indices: {}, but in a future major version, direct access to system "
-                    + "indices will be prevented by default",
-                resolvedSystemIndices
-            );
-        }
-        if (resolvedSystemDataStreams.isEmpty() == false) {
-            throw SystemIndices.dataStreamAccessException(threadContext, resolvedSystemDataStreams);
-        }
-        if (resolvedNetNewSystemIndices.isEmpty() == false) {
-            throw SystemIndices.netNewSystemIndexAccessException(threadContext, resolvedNetNewSystemIndices);
-        }
-    }
-
     private static IndexNotFoundException notFoundException(String... indexExpressions) {
         final IndexNotFoundException infe;
         if (indexExpressions == null
@@ -568,16 +604,16 @@ public class IndexNameExpressionResolver {
     }
 
     private static boolean shouldTrackConcreteIndex(Context context, Index index) {
-        if (context.systemIndexAccessLevel == SystemIndexAccessLevel.BACKWARDS_COMPATIBLE_ONLY
-            && context.netNewSystemIndexPredicate.test(index.getName())) {
+        if (SystemResourceAccess.isNetNewInBackwardCompatibleMode(context, index)) {
             // Exclude this one as it's a net-new system index, and we explicitly don't want those.
             return false;
         }
+        IndicesOptions options = context.getOptions();
         if (DataStream.isFailureStoreFeatureFlagEnabled() && context.options.allowFailureIndices() == false) {
             DataStream parentDataStream = context.getState().metadata().getIndicesLookup().get(index.getName()).getParentDataStream();
             if (parentDataStream != null && parentDataStream.isFailureStoreEnabled()) {
                 if (parentDataStream.isFailureStoreIndex(index.getName())) {
-                    if (context.options.ignoreUnavailable()) {
+                    if (options.ignoreUnavailable()) {
                         return false;
                     } else {
                         throw new FailureIndexNotSupportedException(index);
@@ -587,7 +623,6 @@ public class IndexNameExpressionResolver {
         }
         final IndexMetadata imd = context.state.metadata().index(index);
         if (imd.getState() == IndexMetadata.State.CLOSE) {
-            IndicesOptions options = context.options;
             if (options.forbidClosedIndices() && options.ignoreUnavailable() == false) {
                 throw new IndexClosedException(index);
             } else {
@@ -721,21 +756,6 @@ public class IndexNameExpressionResolver {
         return state.metadata().hasIndexAbstraction(resolvedAliasOrIndex);
     }
 
-    /**
-     * @return If the specified string is data math expression then this method returns the resolved expression.
-     */
-    public static String resolveDateMathExpression(String dateExpression) {
-        return DateMathExpressionResolver.resolveExpression(dateExpression);
-    }
-
-    /**
-     * @param time instant to consider when parsing the expression
-     * @return If the specified string is data math expression then this method returns the resolved expression.
-     */
-    public static String resolveDateMathExpression(String dateExpression, long time) {
-        return DateMathExpressionResolver.resolveExpression(dateExpression, () -> time);
-    }
-
     /**
      * Resolve an array of expressions to the set of indices and aliases that these expressions match.
      */
@@ -765,7 +785,8 @@ public class IndexNameExpressionResolver {
             getSystemIndexAccessPredicate(),
             getNetNewSystemIndexPredicate()
         );
-        Collection<String> resolved = resolveExpressions(context, expressions);
+        // unmodifiable without creating a new collection as it might contain many items
+        Collection<String> resolved = resolveExpressionsToResources(context, expressions);
         if (resolved instanceof Set<String>) {
             // unmodifiable without creating a new collection as it might contain many items
             return Collections.unmodifiableSet((Set<String>) resolved);
@@ -779,7 +800,7 @@ public class IndexNameExpressionResolver {
      * given index.
      * <p>Only aliases with filters are returned. If the indices list contains a non-filtering reference to
      * the index itself - null is returned. Returns {@code null} if no filtering is required.
-     * <b>NOTE</b>: The provided expressions must have been resolved already via {@link #resolveExpressions}.
+     * <b>NOTE</b>: The provided expressions must have been resolved already via {@link #resolveExpressionsToResources(Context, String...)}.
      */
     public String[] filteringAliases(ClusterState state, String index, Set<String> resolvedExpressions) {
         return indexAliases(state, index, AliasMetadata::filteringRequired, DataStreamAlias::filteringRequired, false, resolvedExpressions);
@@ -799,7 +820,8 @@ public class IndexNameExpressionResolver {
      * Iterates through the list of indices and selects the effective list of required aliases for the given index.
      * <p>Only aliases where the given predicate tests successfully are returned. If the indices list contains a non-required reference to
      * the index itself - null is returned. Returns {@code null} if no filtering is required.
-     * <p><b>NOTE</b>: the provided expressions must have been resolved already via {@link #resolveExpressions}.
+     * <p><b>NOTE</b>: the provided expressions must have been resolved already via
+     * {@link #resolveExpressionsToResources(Context, String...)}.
      */
     public String[] indexAliases(
         ClusterState state,
@@ -878,7 +900,8 @@ public class IndexNameExpressionResolver {
                     .toArray(AliasMetadata[]::new);
             }
             List<String> aliases = null;
-            for (AliasMetadata aliasMetadata : aliasCandidates) {
+            for (int i = 0; i < aliasCandidates.length; i++) {
+                AliasMetadata aliasMetadata = aliasCandidates[i];
                 if (requiredAlias.test(aliasMetadata)) {
                     // If required - add it to the list of aliases
                     if (aliases == null) {
@@ -914,7 +937,7 @@ public class IndexNameExpressionResolver {
             getSystemIndexAccessPredicate(),
             getNetNewSystemIndexPredicate()
         );
-        final Collection<String> resolvedExpressions = resolveExpressions(context, expressions);
+        final Collection<String> resolvedExpressions = resolveExpressionsToResources(context, expressions);
 
         // TODO: it appears that this can never be true?
         if (isAllIndices(resolvedExpressions)) {
@@ -932,7 +955,8 @@ public class IndexNameExpressionResolver {
         for (String expression : resolvedExpressions) {
             IndexAbstraction indexAbstraction = state.metadata().getIndicesLookup().get(expression);
             if (indexAbstraction != null && indexAbstraction.getType() == Type.ALIAS) {
-                for (Index index : indexAbstraction.getIndices()) {
+                for (int i = 0, n = indexAbstraction.getIndices().size(); i < n; i++) {
+                    Index index = indexAbstraction.getIndices().get(i);
                     String concreteIndex = index.getName();
                     if (norouting.contains(concreteIndex) == false) {
                         AliasMetadata aliasMetadata = state.metadata().index(concreteIndex).getAliases().get(indexAbstraction.getName());
@@ -961,7 +985,8 @@ public class IndexNameExpressionResolver {
                     continue;
                 }
                 if (dataStream.getIndices() != null) {
-                    for (Index index : dataStream.getIndices()) {
+                    for (int i = 0, n = dataStream.getIndices().size(); i < n; i++) {
+                        Index index = dataStream.getIndices().get(i);
                         String concreteIndex = index.getName();
                         routings = collectRoutings(routings, paramRouting, norouting, concreteIndex);
                     }
@@ -1006,8 +1031,8 @@ public class IndexNameExpressionResolver {
             Set<String> r = Sets.newHashSet(Strings.splitStringByCommaToArray(routing));
             Map<String, Set<String>> routings = new HashMap<>();
             String[] concreteIndices = metadata.getConcreteAllIndices();
-            for (String index : concreteIndices) {
-                routings.put(index, r);
+            for (int i = 0; i < concreteIndices.length; i++) {
+                routings.put(concreteIndices[i], r);
             }
             return routings;
         }
@@ -1036,6 +1061,16 @@ public class IndexNameExpressionResolver {
         return aliasesOrIndices != null && aliasesOrIndices.size() == 1 && Metadata.ALL.equals(aliasesOrIndices.iterator().next());
     }
 
+    /**
+     * Identifies if this expression list is *,-* which effectively means a request that requests no indices.
+     */
+    static boolean isNoneExpression(String[] expressions) {
+        return expressions.length == 2 && "*".equals(expressions[0]) && "-*".equals(expressions[1]);
+    }
+
+    /**
+     * @return the system access level that will be applied in this resolution. See {@link SystemIndexAccessLevel} for details.
+     */
     public SystemIndexAccessLevel getSystemIndexAccessLevel() {
         final SystemIndexAccessLevel accessLevel = SystemIndices.getSystemIndexAccessLevel(threadContext);
         assert accessLevel != SystemIndexAccessLevel.BACKWARDS_COMPATIBLE_ONLY
@@ -1043,6 +1078,14 @@ public class IndexNameExpressionResolver {
         return accessLevel;
     }
 
+    /**
+     * Determines the right predicate based on the {@link IndexNameExpressionResolver#getSystemIndexAccessLevel()}. Specifically:
+     * - NONE implies no access to net-new system indices and data streams
+     * - BACKWARDS_COMPATIBLE_ONLY allows access also to net-new system resources
+     * - ALL allows access to everything
+     * - otherwise we fall back to {@link SystemIndices#getProductSystemIndexNamePredicate(ThreadContext)}
+     * @return the predicate that defines the access to system indices.
+     */
     public Predicate<String> getSystemIndexAccessPredicate() {
         final SystemIndexAccessLevel systemIndexAccessLevel = getSystemIndexAccessLevel();
         final Predicate<String> systemIndexAccessLevelPredicate;
@@ -1067,6 +1110,43 @@ public class IndexNameExpressionResolver {
         return systemIndices::isNetNewSystemIndex;
     }
 
+    /**
+     * This returns `true` if the given {@param name} is of a resource that exists.
+     * Otherwise, it returns `false` if the `ignore_unvailable` option is `true`, or, if `false`, it throws a "not found" type of
+     * exception.
+     */
+    @Nullable
+    private static boolean ensureAliasOrIndexExists(Context context, String name) {
+        boolean ignoreUnavailable = context.getOptions().ignoreUnavailable();
+        IndexAbstraction indexAbstraction = context.getState().getMetadata().getIndicesLookup().get(name);
+        if (indexAbstraction == null) {
+            if (ignoreUnavailable) {
+                return false;
+            } else {
+                throw notFoundException(name);
+            }
+        }
+        // treat aliases as unavailable indices when ignoreAliases is set to true (e.g. delete index and update aliases api)
+        if (indexAbstraction.getType() == Type.ALIAS && context.getOptions().ignoreAliases()) {
+            if (ignoreUnavailable) {
+                return false;
+            } else {
+                throw aliasesNotSupportedException(name);
+            }
+        }
+        if (indexAbstraction.isDataStreamRelated() && context.includeDataStreams() == false) {
+            if (ignoreUnavailable) {
+                return false;
+            } else {
+                IndexNotFoundException infe = notFoundException(name);
+                // Allows callers to handle IndexNotFoundException differently based on whether data streams were excluded.
+                infe.addMetadata(EXCLUDED_DATA_STREAMS_KEY, "true");
+                throw infe;
+            }
+        }
+        return true;
+    }
+
     public static class Context {
 
         private final ClusterState state;
@@ -1242,7 +1322,7 @@ public class IndexNameExpressionResolver {
     }
 
     /**
-     * Resolves alias/index name expressions with wildcards into the corresponding concrete indices/aliases
+     * Resolves name expressions with wildcards into the corresponding concrete indices/aliases/data streams
      */
     static final class WildcardExpressionResolver {
 
@@ -1251,8 +1331,8 @@ public class IndexNameExpressionResolver {
         }
 
         /**
-         * Returns all the indices, datastreams, and aliases, considering the open/closed, system, and hidden context parameters.
-         * Depending on the context, returns the names of the datastreams themselves or their backing indices.
+         * Returns all the indices, data streams, and aliases, considering the open/closed, system, and hidden context parameters.
+         * Depending on the context, returns the names of the data streams themselves or their backing indices.
          */
         public static Collection<String> resolveAll(Context context) {
             List<String> concreteIndices = resolveEmptyOrTrivialWildcard(context);
@@ -1261,16 +1341,17 @@ public class IndexNameExpressionResolver {
                 return concreteIndices;
             }
 
-            Stream<IndexAbstraction> ias = context.getState()
+            Set<String> resolved = new HashSet<>(concreteIndices.size());
+            context.getState()
                 .metadata()
                 .getIndicesLookup()
                 .values()
                 .stream()
                 .filter(ia -> context.getOptions().expandWildcardsHidden() || ia.isHidden() == false)
                 .filter(ia -> shouldIncludeIfDataStream(ia, context) || shouldIncludeIfAlias(ia, context))
-                .filter(ia -> ia.isSystem() == false || context.systemIndexAccessPredicate.test(ia.getName()));
+                .filter(ia -> ia.isSystem() == false || context.systemIndexAccessPredicate.test(ia.getName()))
+                .forEach(ia -> resolved.addAll(expandToOpenClosed(context, ia)));
 
-            Set<String> resolved = expandToOpenClosed(context, ias).collect(Collectors.toSet());
             resolved.addAll(concreteIndices);
             return resolved;
         }
@@ -1283,73 +1364,6 @@ public class IndexNameExpressionResolver {
             return context.getOptions().ignoreAliases() == false && ia.getType() == Type.ALIAS;
         }
 
-        /**
-         * Returns all the existing resource (index, alias and datastream) names that the {@param expressions} list resolves to.
-         * The passed-in {@param expressions} can contain wildcards and exclusions, as well as plain resource names.
-         * <br>
-         * The return is a {@code Collection} (usually a {@code Set} but can also be a {@code List}, for performance reasons) of plain
-         * resource names only. All the returned resources are "accessible", in the given context, i.e. the resources exist
-         * and are not an alias or a datastream if the context does not permit it.
-         * Wildcard expressions, depending on the context:
-         * <ol>
-         *   <li>might throw an exception if they don't resolve to anything</li>
-         *   <li>might not resolve to hidden or system resources (but plain names can refer to hidden or system resources)</li>
-         *   <li>might resolve to aliases and datastreams, and it could be (depending on the context) that their backing indices are what's
-         * ultimately returned, instead of the alias or datastream name</li>
-         * </ol>
-         */
-        public static Collection<String> resolve(Context context, List<String> expressions) {
-            // fast exit if there are no wildcards to evaluate
-            if (context.getOptions().expandWildcardExpressions() == false) {
-                return expressions;
-            }
-            int firstWildcardIndex = 0;
-            for (; firstWildcardIndex < expressions.size(); firstWildcardIndex++) {
-                String expression = expressions.get(firstWildcardIndex);
-                if (isWildcard(expression)) {
-                    break;
-                }
-            }
-            if (firstWildcardIndex == expressions.size()) {
-                return expressions;
-            }
-            Set<String> result = new HashSet<>();
-            for (int i = 0; i < firstWildcardIndex; i++) {
-                result.add(expressions.get(i));
-            }
-            AtomicBoolean emptyWildcardExpansion = context.getOptions().allowNoIndices() ? null : new AtomicBoolean();
-            for (int i = firstWildcardIndex; i < expressions.size(); i++) {
-                String expression = expressions.get(i);
-                boolean isExclusion = i > firstWildcardIndex && expression.charAt(0) == '-';
-                if (i == firstWildcardIndex || isWildcard(expression)) {
-                    Stream<IndexAbstraction> matchingResources = matchResourcesToWildcard(
-                        context,
-                        isExclusion ? expression.substring(1) : expression
-                    );
-                    Stream<String> matchingOpenClosedNames = expandToOpenClosed(context, matchingResources);
-                    if (emptyWildcardExpansion != null) {
-                        emptyWildcardExpansion.set(true);
-                        matchingOpenClosedNames = matchingOpenClosedNames.peek(x -> emptyWildcardExpansion.set(false));
-                    }
-                    if (isExclusion) {
-                        matchingOpenClosedNames.forEach(result::remove);
-                    } else {
-                        matchingOpenClosedNames.forEach(result::add);
-                    }
-                    if (emptyWildcardExpansion != null && emptyWildcardExpansion.get()) {
-                        throw notFoundException(expression);
-                    }
-                } else {
-                    if (isExclusion) {
-                        result.remove(expression.substring(1));
-                    } else {
-                        result.add(expression);
-                    }
-                }
-            }
-            return result;
-        }
-
         private static IndexMetadata.State excludeState(IndicesOptions options) {
             final IndexMetadata.State excludeState;
             if (options.expandWildcardsOpen() && options.expandWildcardsClosed()) {
@@ -1366,55 +1380,82 @@ public class IndexNameExpressionResolver {
         }
 
         /**
-         * Given a single wildcard {@param expression}, return the {@code Stream} that contains all the resources (i.e. indices, aliases,
-         * and datastreams), that exist in the cluster at this moment in time, and that the wildcard "resolves" to (i.e. the resource's
+         * Given a single wildcard {@param expression}, return a {@code Set} that contains all the resources (i.e. indices, aliases,
+         * and data streams), that exist in the cluster at this moment in time, and that the wildcard "resolves" to (i.e. the resource's
          * name matches the {@param expression} wildcard).
          * The {@param context} provides the current time-snapshot view of cluster state, as well as conditions
-         * on whether to consider alias, datastream, system, and hidden resources.
-         * It does NOT consider the open or closed status of index resources.
+         * on whether to consider alias, data stream, system, and hidden resources.
          */
-        private static Stream<IndexAbstraction> matchResourcesToWildcard(Context context, String wildcardExpression) {
+        static Set<String> matchWildcardToResources(Context context, String wildcardExpression) {
             assert isWildcard(wildcardExpression);
             final SortedMap<String, IndexAbstraction> indicesLookup = context.getState().getMetadata().getIndicesLookup();
-            Stream<IndexAbstraction> matchesStream;
+            Set<String> matchedResources = new HashSet<>();
+            // this applies an initial pre-filtering in the case where the expression is a common suffix wildcard, eg "test*"
             if (Regex.isSuffixMatchPattern(wildcardExpression)) {
-                // this is an initial pre-filtering in the case where the expression is a common suffix wildcard, eg "test*"
-                matchesStream = filterIndicesLookupForSuffixWildcard(indicesLookup, wildcardExpression).values().stream();
-            } else {
-                matchesStream = indicesLookup.values().stream();
-                if (Regex.isMatchAllPattern(wildcardExpression) == false) {
-                    matchesStream = matchesStream.filter(
-                        indexAbstraction -> Regex.simpleMatch(wildcardExpression, indexAbstraction.getName())
-                    );
+                for (IndexAbstraction ia : filterIndicesLookupForSuffixWildcard(indicesLookup, wildcardExpression).values()) {
+                    maybeAddToResult(context, wildcardExpression, ia, matchedResources);
+                }
+                return matchedResources;
+            }
+            // In case of match all it fetches all index abstractions
+            if (Regex.isMatchAllPattern(wildcardExpression)) {
+                for (IndexAbstraction ia : indicesLookup.values()) {
+                    maybeAddToResult(context, wildcardExpression, ia, matchedResources);
                 }
+                return matchedResources;
             }
-            if (context.getOptions().ignoreAliases()) {
-                matchesStream = matchesStream.filter(indexAbstraction -> indexAbstraction.getType() != Type.ALIAS);
+            for (IndexAbstraction indexAbstraction : indicesLookup.values()) {
+                if (Regex.simpleMatch(wildcardExpression, indexAbstraction.getName())) {
+                    maybeAddToResult(context, wildcardExpression, indexAbstraction, matchedResources);
+                }
             }
-            if (context.includeDataStreams() == false) {
-                matchesStream = matchesStream.filter(indexAbstraction -> indexAbstraction.isDataStreamRelated() == false);
+            return matchedResources;
+        }
+
+        private static void maybeAddToResult(
+            Context context,
+            String wildcardExpression,
+            IndexAbstraction indexAbstraction,
+            Set<String> matchedResources
+        ) {
+            if (shouldExpandToIndexAbstraction(context, wildcardExpression, indexAbstraction)) {
+                matchedResources.addAll(expandToOpenClosed(context, indexAbstraction));
             }
-            // historic, i.e. not net-new, system indices are included irrespective of the system access predicate
-            // the system access predicate is based on the endpoint kind and HTTP request headers that identify the stack feature
-            matchesStream = matchesStream.filter(
-                indexAbstraction -> indexAbstraction.isSystem() == false
-                    || (indexAbstraction.getType() != Type.DATA_STREAM
-                        && indexAbstraction.getParentDataStream() == null
-                        && context.netNewSystemIndexPredicate.test(indexAbstraction.getName()) == false)
-                    || context.systemIndexAccessPredicate.test(indexAbstraction.getName())
-            );
+        }
+
+        /**
+         * Checks if this index abstraction should be included because it matched the wildcard expression.
+         * @param context the options of this request that influence the decision if this index abstraction should be included in the result
+         * @param wildcardExpression the wildcard expression that matched this index abstraction
+         * @param indexAbstraction the index abstraction in question
+         * @return true, if the index abstraction should be included in the result
+         */
+        private static boolean shouldExpandToIndexAbstraction(
+            Context context,
+            String wildcardExpression,
+            IndexAbstraction indexAbstraction
+        ) {
+            if (context.getOptions().ignoreAliases() && indexAbstraction.getType() == Type.ALIAS) {
+                return false;
+            }
+            if (context.includeDataStreams() == false && indexAbstraction.isDataStreamRelated()) {
+                return false;
+            }
+
+            if (indexAbstraction.isSystem()
+                && SystemResourceAccess.shouldExpandToSystemIndexAbstraction(context, indexAbstraction) == false) {
+                return false;
+            }
+
             if (context.getOptions().expandWildcardsHidden() == false) {
-                if (wildcardExpression.startsWith(".")) {
-                    // there is this behavior that hidden indices that start with "." are not hidden if the wildcard expression also
-                    // starts with "."
-                    matchesStream = matchesStream.filter(
-                        indexAbstraction -> indexAbstraction.isHidden() == false || indexAbstraction.getName().startsWith(".")
-                    );
-                } else {
-                    matchesStream = matchesStream.filter(indexAbstraction -> indexAbstraction.isHidden() == false);
+                // there is this behavior that hidden indices that start with "." are not hidden if the wildcard expression also
+                // starts with "."
+                if (indexAbstraction.isHidden()
+                    && (wildcardExpression.startsWith(".") && indexAbstraction.getName().startsWith(".")) == false) {
+                    return false;
                 }
             }
-            return matchesStream;
+            return true;
         }
 
         private static Map<String, IndexAbstraction> filterIndicesLookupForSuffixWildcard(
@@ -1430,35 +1471,39 @@ public class IndexNameExpressionResolver {
         }
 
         /**
-         * Return the {@code Stream} of open and/or closed index names for the given {@param resources}.
+         * Return the {@code Set} of open and/or closed index names for the given {@param resources}.
          * Data streams and aliases are interpreted to refer to multiple indices,
          * then all index resources are filtered by their open/closed status.
          */
-        private static Stream<String> expandToOpenClosed(Context context, Stream<IndexAbstraction> resources) {
+        private static Set<String> expandToOpenClosed(Context context, IndexAbstraction indexAbstraction) {
             final IndexMetadata.State excludeState = excludeState(context.getOptions());
-            return resources.flatMap(indexAbstraction -> {
-                if (context.isPreserveAliases() && indexAbstraction.getType() == Type.ALIAS) {
-                    return Stream.of(indexAbstraction.getName());
-                } else if (context.isPreserveDataStreams() && indexAbstraction.getType() == Type.DATA_STREAM) {
-                    return Stream.of(indexAbstraction.getName());
-                } else {
-                    Stream<IndexMetadata> indicesStateStream = Stream.of();
-                    if (shouldIncludeRegularIndices(context.getOptions())) {
-                        indicesStateStream = indexAbstraction.getIndices().stream().map(context.state.metadata()::index);
-                    }
-                    if (indexAbstraction.getType() == Type.DATA_STREAM && shouldIncludeFailureIndices(context.getOptions())) {
-                        DataStream dataStream = (DataStream) indexAbstraction;
-                        indicesStateStream = Stream.concat(
-                            indicesStateStream,
-                            dataStream.getFailureIndices().getIndices().stream().map(context.state.metadata()::index)
-                        );
+            Set<String> resources = new HashSet<>();
+            if (context.isPreserveAliases() && indexAbstraction.getType() == Type.ALIAS) {
+                resources.add(indexAbstraction.getName());
+            } else if (context.isPreserveDataStreams() && indexAbstraction.getType() == Type.DATA_STREAM) {
+                resources.add(indexAbstraction.getName());
+            } else {
+                if (shouldIncludeRegularIndices(context.getOptions())) {
+                    for (int i = 0, n = indexAbstraction.getIndices().size(); i < n; i++) {
+                        Index index = indexAbstraction.getIndices().get(i);
+                        IndexMetadata indexMetadata = context.state.metadata().index(index);
+                        if (indexMetadata.getState() != excludeState) {
+                            resources.add(index.getName());
+                        }
                     }
-                    if (excludeState != null) {
-                        indicesStateStream = indicesStateStream.filter(indexMeta -> indexMeta.getState() != excludeState);
+                }
+                if (indexAbstraction.getType() == Type.DATA_STREAM && shouldIncludeFailureIndices(context.getOptions())) {
+                    DataStream dataStream = (DataStream) indexAbstraction;
+                    for (int i = 0, n = dataStream.getFailureIndices().getIndices().size(); i < n; i++) {
+                        Index index = dataStream.getFailureIndices().getIndices().get(i);
+                        IndexMetadata indexMetadata = context.state.metadata().index(index);
+                        if (indexMetadata.getState() != excludeState) {
+                            resources.add(index.getName());
+                        }
                     }
-                    return indicesStateStream.map(indexMeta -> indexMeta.getIndex().getName());
                 }
-            });
+            }
+            return resources;
         }
 
         private static List<String> resolveEmptyOrTrivialWildcard(Context context) {
@@ -1471,26 +1516,26 @@ public class IndexNameExpressionResolver {
         }
 
         private static List<String> resolveEmptyOrTrivialWildcardWithAllowedSystemIndices(Context context, String[] allIndices) {
-            return Arrays.stream(allIndices).filter(name -> {
-                if (name.startsWith(".")) {
-                    IndexAbstraction abstraction = context.state.metadata().getIndicesLookup().get(name);
-                    assert abstraction != null : "null abstraction for " + name + " but was in array of all indices";
-                    if (abstraction.isSystem()) {
-                        if (context.netNewSystemIndexPredicate.test(name)) {
-                            if (SystemIndexAccessLevel.BACKWARDS_COMPATIBLE_ONLY.equals(context.systemIndexAccessLevel)) {
-                                return false;
-                            } else {
-                                return context.systemIndexAccessPredicate.test(name);
-                            }
-                        } else if (abstraction.getType() == Type.DATA_STREAM || abstraction.getParentDataStream() != null) {
-                            return context.systemIndexAccessPredicate.test(name);
-                        }
-                    } else {
-                        return true;
-                    }
+            List<String> filteredIndices = new ArrayList<>(allIndices.length);
+            for (int i = 0; i < allIndices.length; i++) {
+                if (shouldIncludeIndexAbstraction(context, allIndices[i])) {
+                    filteredIndices.add(allIndices[i]);
                 }
+            }
+            return filteredIndices;
+        }
+
+        private static boolean shouldIncludeIndexAbstraction(Context context, String name) {
+            if (name.startsWith(".") == false) {
                 return true;
-            }).toList();
+            }
+
+            IndexAbstraction abstraction = context.state.metadata().getIndicesLookup().get(name);
+            assert abstraction != null : "null abstraction for " + name + " but was in array of all indices";
+            if (abstraction.isSystem() == false) {
+                return true;
+            }
+            return SystemResourceAccess.isSystemIndexAbstractionAccessible(context, abstraction);
         }
 
         private static String[] resolveEmptyOrTrivialWildcardToAllIndices(IndicesOptions options, Metadata metadata) {
@@ -1513,8 +1558,39 @@ public class IndexNameExpressionResolver {
                 return Strings.EMPTY_ARRAY;
             }
         }
+
+        static boolean isWildcard(String expression) {
+            return Regex.isSimpleMatchPattern(expression);
+        }
+
+        static boolean hasWildcards(String[] expressions) {
+            for (int i = 0; i < expressions.length; i++) {
+                if (isWildcard(expressions[i])) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * @return If the specified string is data math expression then this method returns the resolved expression.
+     */
+    public static String resolveDateMathExpression(String dateExpression) {
+        return DateMathExpressionResolver.resolveExpression(dateExpression);
+    }
+
+    /**
+     * @param time instant to consider when parsing the expression
+     * @return If the specified string is data math expression then this method returns the resolved expression.
+     */
+    public static String resolveDateMathExpression(String dateExpression, long time) {
+        return DateMathExpressionResolver.resolveExpression(dateExpression, () -> time);
     }
 
+    /**
+     * Resolves a date math expression based on the requested time.
+     */
     public static final class DateMathExpressionResolver {
 
         private static final DateFormatter DEFAULT_DATE_FORMATTER = DateFormatter.forPattern("uuuu.MM.dd");
@@ -1530,35 +1606,18 @@ public class IndexNameExpressionResolver {
         }
 
         /**
-         * Resolves date math expressions. If this is a noop the given {@code expressions} list is returned without copying.
-         * As a result callers of this method should not mutate the returned list. Mutating it may come with unexpected side effects.
+         * Resolves a date math expression using the current time. This method recognises a date math expression iff when they start with
+         * <code>%3C</code> and end with <code>%3E</code>. Otherwise, it returns the expression intact.
          */
-        public static List<String> resolve(Context context, List<String> expressions) {
-            boolean wildcardSeen = false;
-            final boolean expandWildcards = context.getOptions().expandWildcardExpressions();
-            String[] result = null;
-            for (int i = 0, n = expressions.size(); i < n; i++) {
-                String expression = expressions.get(i);
-                // accepts date-math exclusions that are of the form "-<...{}>",f i.e. the "-" is outside the "<>" date-math template
-                boolean isExclusion = wildcardSeen && expression.startsWith("-");
-                wildcardSeen = wildcardSeen || (expandWildcards && isWildcard(expression));
-                String toResolve = isExclusion ? expression.substring(1) : expression;
-                String resolved = resolveExpression(toResolve, context::getStartTime);
-                if (toResolve != resolved) {
-                    if (result == null) {
-                        result = expressions.toArray(Strings.EMPTY_ARRAY);
-                    }
-                    result[i] = isExclusion ? "-" + resolved : resolved;
-                }
-            }
-            return result == null ? expressions : Arrays.asList(result);
-        }
-
-        static String resolveExpression(String expression) {
+        public static String resolveExpression(String expression) {
             return resolveExpression(expression, System::currentTimeMillis);
         }
 
-        static String resolveExpression(String expression, LongSupplier getTime) {
+        /**
+         * Resolves a date math expression using the provided time. This method recognises a date math expression iff when they start with
+         * <code>%3C</code> and end with <code>%3E</code>. Otherwise, it returns the expression intact.
+         */
+        public static String resolveExpression(String expression, LongSupplier getTime) {
             if (expression.startsWith(EXPRESSION_LEFT_BOUND) == false || expression.endsWith(EXPRESSION_RIGHT_BOUND) == false) {
                 return expression;
             }
@@ -1707,135 +1766,133 @@ public class IndexNameExpressionResolver {
         }
     }
 
-    public static final class ExplicitResourceNameFilter {
+    /**
+     * In this class we collect the system access relevant code. The helper methods provide the following functionalities:
+     * - determining the access to a system index abstraction
+     * - verifying the access to system abstractions and adding the necessary warnings
+     * - determining the access to a system index based on its name
+     * WARNING: we have observed differences in how the access is determined. For now this behaviour is documented and preserved.
+     */
+    public static final class SystemResourceAccess {
 
-        private ExplicitResourceNameFilter() {
+        private SystemResourceAccess() {
             // Utility class
         }
 
         /**
-         * Returns an expression list with "unavailable" (missing or not acceptable) resource names filtered out.
-         * Only explicit resource names are considered for filtering. Wildcard and exclusion expressions are kept in.
+         * Checks if this system index abstraction should be included when resolving via {@link
+         * IndexNameExpressionResolver.WildcardExpressionResolver#resolveEmptyOrTrivialWildcardWithAllowedSystemIndices(Context, String[])}.
+         * NOTE: it behaves differently than {@link SystemResourceAccess#shouldExpandToSystemIndexAbstraction(Context, IndexAbstraction)}
+         * because in the case that the access level is BACKWARDS_COMPATIBLE_ONLY it does not include the net-new indices, this is
+         * questionable.
          */
-        public static List<String> filterUnavailable(Context context, List<String> expressions) {
-            ensureRemoteIndicesRequireIgnoreUnavailable(context.getOptions(), expressions);
-            final boolean expandWildcards = context.getOptions().expandWildcardExpressions();
-            boolean wildcardSeen = false;
-            List<String> result = null;
-            for (int i = 0; i < expressions.size(); i++) {
-                String expression = expressions.get(i);
-                if (Strings.isEmpty(expression)) {
-                    throw notFoundException(expression);
-                }
-                // Expressions can not start with an underscore. This is reserved for APIs. If the check gets here, the API
-                // does not exist and the path is interpreted as an expression. If the expression begins with an underscore,
-                // throw a specific error that is different from the [[IndexNotFoundException]], which is typically thrown
-                // if the expression can't be found.
-                if (expression.charAt(0) == '_') {
-                    throw new InvalidIndexNameException(expression, "must not start with '_'.");
-                }
-                final boolean isWildcard = expandWildcards && isWildcard(expression);
-                if (isWildcard || (wildcardSeen && expression.charAt(0) == '-') || ensureAliasOrIndexExists(context, expression)) {
-                    if (result != null) {
-                        result.add(expression);
-                    }
+        public static boolean isSystemIndexAbstractionAccessible(Context context, IndexAbstraction abstraction) {
+            assert abstraction.isSystem() : "We should only check this for system resources";
+            if (context.netNewSystemIndexPredicate.test(abstraction.getName())) {
+                if (SystemIndexAccessLevel.BACKWARDS_COMPATIBLE_ONLY.equals(context.systemIndexAccessLevel)) {
+                    return false;
                 } else {
-                    if (result == null) {
-                        result = new ArrayList<>(expressions.size() - 1);
-                        result.addAll(expressions.subList(0, i));
-                    }
+                    return context.systemIndexAccessPredicate.test(abstraction.getName());
                 }
-                wildcardSeen |= isWildcard;
+            } else if (abstraction.getType() == Type.DATA_STREAM || abstraction.getParentDataStream() != null) {
+                return context.systemIndexAccessPredicate.test(abstraction.getName());
             }
-            return result == null ? expressions : result;
+            return true;
         }
 
         /**
-         * This returns `true` if the given {@param name} is of a resource that exists.
-         * Otherwise, it returns `false` if the `ignore_unvailable` option is `true`, or, if `false`, it throws a "not found" type of
-         * exception.
+         * Historic, i.e. not net-new, system indices are included irrespective of the system access predicate
+         * the system access predicate is based on the endpoint kind and HTTP request headers that identify the stack feature.
+         * A historic system resource, can only be an index since system data streams were added later.
          */
-        @Nullable
-        private static boolean ensureAliasOrIndexExists(Context context, String name) {
-            boolean ignoreUnavailable = context.getOptions().ignoreUnavailable();
-            IndexAbstraction indexAbstraction = context.getState().getMetadata().getIndicesLookup().get(name);
-            if (indexAbstraction == null) {
-                if (ignoreUnavailable) {
-                    return false;
-                } else {
-                    throw notFoundException(name);
-                }
-            }
-            // treat aliases as unavailable indices when ignoreAliases is set to true (e.g. delete index and update aliases api)
-            if (indexAbstraction.getType() == Type.ALIAS && context.getOptions().ignoreAliases()) {
-                if (ignoreUnavailable) {
-                    return false;
-                } else {
-                    throw aliasesNotSupportedException(name);
-                }
-            }
-            if (indexAbstraction.isDataStreamRelated() && context.includeDataStreams() == false) {
-                if (ignoreUnavailable) {
-                    return false;
-                } else {
-                    IndexNotFoundException infe = notFoundException(name);
-                    // Allows callers to handle IndexNotFoundException differently based on whether data streams were excluded.
-                    infe.addMetadata(EXCLUDED_DATA_STREAMS_KEY, "true");
-                    throw infe;
-                }
-            }
-            return true;
+        private static boolean shouldExpandToSystemIndexAbstraction(Context context, IndexAbstraction indexAbstraction) {
+            assert indexAbstraction.isSystem() : "We should only check this for system resources";
+            boolean isHistoric = indexAbstraction.getType() != Type.DATA_STREAM
+                && indexAbstraction.getParentDataStream() == null
+                && context.netNewSystemIndexPredicate.test(indexAbstraction.getName()) == false;
+            return isHistoric || context.systemIndexAccessPredicate.test(indexAbstraction.getName());
         }
 
-        private static void ensureRemoteIndicesRequireIgnoreUnavailable(IndicesOptions options, List<String> indexExpressions) {
-            if (options.ignoreUnavailable()) {
+        /**
+         * Checks if any system indices that should not have been accessible according to the
+         * {@link Context#getSystemIndexAccessPredicate()} are accessed, and it performs the following actions:
+         * - if there are historic (aka not net-new) system indices, then it adds a deprecation warning
+         * - if it contains net-new system indices or system data streams, it throws an exception.
+         */
+        private static void checkSystemIndexAccess(Context context, ThreadContext threadContext, Index... concreteIndices) {
+            final Predicate<String> systemIndexAccessPredicate = context.getSystemIndexAccessPredicate();
+            if (systemIndexAccessPredicate == Predicates.<String>always()) {
                 return;
             }
-            for (String index : indexExpressions) {
-                if (RemoteClusterAware.isRemoteIndexName(index)) {
-                    failOnRemoteIndicesNotIgnoringUnavailable(indexExpressions);
-                }
-            }
+            doCheckSystemIndexAccess(context, systemIndexAccessPredicate, threadContext, concreteIndices);
         }
 
-        private static void failOnRemoteIndicesNotIgnoringUnavailable(List<String> indexExpressions) {
-            List<String> crossClusterIndices = new ArrayList<>();
-            for (String index : indexExpressions) {
-                if (RemoteClusterAware.isRemoteIndexName(index)) {
-                    crossClusterIndices.add(index);
+        private static void doCheckSystemIndexAccess(
+            Context context,
+            Predicate<String> systemIndexAccessPredicate,
+            ThreadContext threadContext,
+            Index... concreteIndices
+        ) {
+            final Metadata metadata = context.getState().metadata();
+            final List<String> resolvedSystemIndices = new ArrayList<>();
+            final List<String> resolvedNetNewSystemIndices = new ArrayList<>();
+            final Set<String> resolvedSystemDataStreams = new HashSet<>();
+            final SortedMap<String, IndexAbstraction> indicesLookup = metadata.getIndicesLookup();
+            boolean matchedIndex = false;
+            for (int i = 0; i < concreteIndices.length; i++) {
+                Index concreteIndex = concreteIndices[i];
+                IndexMetadata idxMetadata = metadata.index(concreteIndex);
+                String name = concreteIndex.getName();
+                if (idxMetadata.isSystem() && systemIndexAccessPredicate.test(name) == false) {
+                    matchedIndex = true;
+                    IndexAbstraction indexAbstraction = indicesLookup.get(name);
+                    if (indexAbstraction.getParentDataStream() != null) {
+                        resolvedSystemDataStreams.add(indexAbstraction.getParentDataStream().getName());
+                    } else if (context.netNewSystemIndexPredicate.test(name)) {
+                        resolvedNetNewSystemIndices.add(name);
+                    } else {
+                        resolvedSystemIndices.add(name);
+                    }
                 }
             }
-            throw new IllegalArgumentException(
-                "Cross-cluster calls are not supported in this context but remote indices were requested: " + crossClusterIndices
-            );
-        }
-    }
-
-    /**
-     * This is a context for the DateMathExpressionResolver which does not require {@code IndicesOptions} or {@code ClusterState}
-     * since it uses only the start time to resolve expressions.
-     */
-    public static final class ResolverContext extends Context {
-        public ResolverContext() {
-            this(System.currentTimeMillis());
-        }
-
-        public ResolverContext(long startTime) {
-            super(null, null, startTime, false, false, false, false, SystemIndexAccessLevel.ALL, Predicates.never(), Predicates.never());
+            if (matchedIndex) {
+                handleMatchedSystemIndices(resolvedSystemIndices, resolvedSystemDataStreams, resolvedNetNewSystemIndices, threadContext);
+            }
         }
 
-        @Override
-        public ClusterState getState() {
-            throw new UnsupportedOperationException("should never be called");
+        private static void handleMatchedSystemIndices(
+            List<String> resolvedSystemIndices,
+            Set<String> resolvedSystemDataStreams,
+            List<String> resolvedNetNewSystemIndices,
+            ThreadContext threadContext
+        ) {
+            if (resolvedSystemIndices.isEmpty() == false) {
+                Collections.sort(resolvedSystemIndices);
+                deprecationLogger.warn(
+                    DeprecationCategory.API,
+                    "open_system_index_access",
+                    "this request accesses system indices: {}, but in a future major version, direct access to system "
+                        + "indices will be prevented by default",
+                    resolvedSystemIndices
+                );
+            }
+            if (resolvedSystemDataStreams.isEmpty() == false) {
+                throw SystemIndices.dataStreamAccessException(threadContext, resolvedSystemDataStreams);
+            }
+            if (resolvedNetNewSystemIndices.isEmpty() == false) {
+                throw SystemIndices.netNewSystemIndexAccessException(threadContext, resolvedNetNewSystemIndices);
+            }
         }
 
-        @Override
-        public IndicesOptions getOptions() {
-            throw new UnsupportedOperationException("should never be called");
+        /**
+         * Used in {@link IndexNameExpressionResolver#shouldTrackConcreteIndex(Context, Index)} to exclude net-new indices
+         * when we are in backwards compatible only access level.
+         * This also feels questionable as well.
+         */
+        private static boolean isNetNewInBackwardCompatibleMode(Context context, Index index) {
+            return context.systemIndexAccessLevel == SystemIndexAccessLevel.BACKWARDS_COMPATIBLE_ONLY
+                && context.netNewSystemIndexPredicate.test(index.getName());
         }
     }
 
-    private static boolean isWildcard(String expression) {
-        return Regex.isSimpleMatchPattern(expression);
-    }
 }

+ 64 - 137
server/src/test/java/org/elasticsearch/cluster/metadata/DateMathExpressionResolverTests.java

@@ -10,163 +10,90 @@
 package org.elasticsearch.cluster.metadata;
 
 import org.elasticsearch.ElasticsearchParseException;
-import org.elasticsearch.action.support.IndicesOptions;
-import org.elasticsearch.cluster.ClusterName;
-import org.elasticsearch.cluster.ClusterState;
-import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.Context;
 import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.DateMathExpressionResolver;
-import org.elasticsearch.indices.SystemIndices.SystemIndexAccessLevel;
 import org.elasticsearch.test.ESTestCase;
-import org.hamcrest.Matchers;
 
 import java.time.Instant;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
 import java.util.Locale;
+import java.util.function.LongSupplier;
 
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 
 public class DateMathExpressionResolverTests extends ESTestCase {
 
-    private final Context context = new Context(
-        ClusterState.builder(new ClusterName("_name")).build(),
-        IndicesOptions.strictExpand(),
-        SystemIndexAccessLevel.NONE
-    );
+    private final long now = randomMillisUpToYear9999();
+    private final LongSupplier getTime = () -> now;
 
-    private static ZonedDateTime dateFromMillis(long millis) {
-        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.UTC);
-    }
+    public void testNoDateMathExpression() {
+        String expression = randomAlphaOfLength(10);
+        assertThat(DateMathExpressionResolver.resolveExpression(expression, getTime), equalTo(expression));
 
-    private static String formatDate(String pattern, ZonedDateTime zonedDateTime) {
-        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(pattern, Locale.ROOT);
-        return dateFormatter.format(zonedDateTime);
+        expression = "*";
+        assertThat(DateMathExpressionResolver.resolveExpression(expression, getTime), equalTo(expression));
     }
 
-    public void testNormal() throws Exception {
-        int numIndexExpressions = randomIntBetween(1, 9);
-        List<String> indexExpressions = new ArrayList<>(numIndexExpressions);
-        for (int i = 0; i < numIndexExpressions; i++) {
-            indexExpressions.add(randomAlphaOfLength(10));
-        }
-        List<String> result = DateMathExpressionResolver.resolve(context, indexExpressions);
-        assertThat(result.size(), equalTo(indexExpressions.size()));
-        for (int i = 0; i < indexExpressions.size(); i++) {
-            assertThat(result.get(i), equalTo(indexExpressions.get(i)));
-        }
-    }
+    public void testExpression() {
+        String result = DateMathExpressionResolver.resolveExpression("<.marvel-{now}>", getTime);
+        assertThat(result, equalTo(".marvel-" + formatDate("uuuu.MM.dd", dateFromMillis(now))));
 
-    public void testExpression() throws Exception {
-        List<String> indexExpressions = Arrays.asList("<.marvel-{now}>", "<.watch_history-{now}>", "<logstash-{now}>");
-        List<String> result = DateMathExpressionResolver.resolve(context, indexExpressions);
-        assertThat(result.size(), equalTo(3));
-        assertThat(result.get(0), equalTo(".marvel-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()))));
-        assertThat(result.get(1), equalTo(".watch_history-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()))));
-        assertThat(result.get(2), equalTo("logstash-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()))));
+        result = DateMathExpressionResolver.resolveExpression("<.watch_history-{now}>", getTime);
+        assertThat(result, equalTo(".watch_history-" + formatDate("uuuu.MM.dd", dateFromMillis(now))));
+
+        result = DateMathExpressionResolver.resolveExpression("<logstash-{now}>", getTime);
+        assertThat(result, equalTo("logstash-" + formatDate("uuuu.MM.dd", dateFromMillis(now))));
     }
 
     public void testExpressionWithWildcardAndExclusions() {
-        List<String> indexExpressions = Arrays.asList(
-            "<-before-inner-{now}>",
-            "-<before-outer-{now}>",
-            "<wild*card-{now}*>",
-            "<-after-inner-{now}>",
-            "-<after-outer-{now}>"
-        );
-        List<String> result = DateMathExpressionResolver.resolve(context, indexExpressions);
-        assertThat(
-            result,
-            Matchers.contains(
-                equalTo("-before-inner-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()))),
-                equalTo("-<before-outer-{now}>"), // doesn't evaluate because it doesn't start with "<" and it is not an exclusion
-                equalTo("wild*card-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime())) + "*"),
-                equalTo("-after-inner-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()))),
-                equalTo("-after-outer-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime())))
-            )
-        );
-        Context noWildcardExpandContext = new Context(
-            ClusterState.builder(new ClusterName("_name")).build(),
-            IndicesOptions.strictSingleIndexNoExpandForbidClosed(),
-            SystemIndexAccessLevel.NONE
-        );
-        result = DateMathExpressionResolver.resolve(noWildcardExpandContext, indexExpressions);
-        assertThat(
-            result,
-            Matchers.contains(
-                equalTo("-before-inner-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()))),
-                // doesn't evaluate because it doesn't start with "<" and there can't be exclusions without wildcard expansion
-                equalTo("-<before-outer-{now}>"),
-                equalTo("wild*card-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime())) + "*"),
-                equalTo("-after-inner-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()))),
-                // doesn't evaluate because it doesn't start with "<" and there can't be exclusions without wildcard expansion
-                equalTo("-<after-outer-{now}>")
-            )
-        );
-    }
+        String result = DateMathExpressionResolver.resolveExpression("<-before-inner-{now}>", getTime);
+        assertThat(result, equalTo("-before-inner-" + formatDate("uuuu.MM.dd", dateFromMillis(now))));
+
+        result = DateMathExpressionResolver.resolveExpression("<wild*card-{now}*>", getTime);
+        assertThat(result, equalTo("wild*card-" + formatDate("uuuu.MM.dd", dateFromMillis(now)) + "*"));
+
+        result = DateMathExpressionResolver.resolveExpression("<-after-inner-{now}>", getTime);
+        assertThat(result, equalTo("-after-inner-" + formatDate("uuuu.MM.dd", dateFromMillis(now))));
 
-    public void testEmpty() throws Exception {
-        List<String> result = DateMathExpressionResolver.resolve(context, Collections.<String>emptyList());
-        assertThat(result.size(), equalTo(0));
     }
 
-    public void testExpression_Static() throws Exception {
-        List<String> result = DateMathExpressionResolver.resolve(context, Arrays.asList("<.marvel-test>"));
-        assertThat(result.size(), equalTo(1));
-        assertThat(result.get(0), equalTo(".marvel-test"));
+    public void testExpression_Static() {
+        String result = DateMathExpressionResolver.resolveExpression("<.marvel-test>", getTime);
+        assertThat(result, equalTo(".marvel-test"));
     }
 
-    public void testExpression_MultiParts() throws Exception {
-        List<String> result = DateMathExpressionResolver.resolve(context, Arrays.asList("<.text1-{now/d}-text2-{now/M}>"));
-        assertThat(result.size(), equalTo(1));
+    public void testExpression_MultiParts() {
+        String result = DateMathExpressionResolver.resolveExpression("<.text1-{now/d}-text2-{now/M}>", getTime);
         assertThat(
-            result.get(0),
+            result,
             equalTo(
                 ".text1-"
-                    + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()))
+                    + formatDate("uuuu.MM.dd", dateFromMillis(now))
                     + "-text2-"
-                    + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()).withDayOfMonth(1))
+                    + formatDate("uuuu.MM.dd", dateFromMillis(now).withDayOfMonth(1))
             )
         );
     }
 
-    public void testExpression_CustomFormat() throws Exception {
-        List<String> results = DateMathExpressionResolver.resolve(context, Arrays.asList("<.marvel-{now/d{yyyy.MM.dd}}>"));
-        assertThat(results.size(), equalTo(1));
-        assertThat(results.get(0), equalTo(".marvel-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()))));
-    }
-
-    public void testExpression_EscapeStatic() throws Exception {
-        List<String> result = DateMathExpressionResolver.resolve(context, Arrays.asList("<.mar\\{v\\}el-{now/d}>"));
-        assertThat(result.size(), equalTo(1));
-        assertThat(result.get(0), equalTo(".mar{v}el-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()))));
+    public void testExpression_CustomFormat() {
+        String result = DateMathExpressionResolver.resolveExpression("<.marvel-{now/d{yyyy.MM.dd}}>", getTime);
+        assertThat(result, equalTo(".marvel-" + formatDate("uuuu.MM.dd", dateFromMillis(now))));
     }
 
-    public void testExpression_EscapeDateFormat() throws Exception {
-        List<String> result = DateMathExpressionResolver.resolve(context, Arrays.asList("<.marvel-{now/d{'\\{year\\}'yyyy}}>"));
-        assertThat(result.size(), equalTo(1));
-        assertThat(result.get(0), equalTo(".marvel-" + formatDate("'{year}'yyyy", dateFromMillis(context.getStartTime()))));
+    public void testExpression_EscapeStatic() {
+        String result = DateMathExpressionResolver.resolveExpression("<.mar\\{v\\}el-{now/d}>", getTime);
+        assertThat(result, equalTo(".mar{v}el-" + formatDate("uuuu.MM.dd", dateFromMillis(now))));
     }
 
-    public void testExpression_MixedArray() throws Exception {
-        List<String> result = DateMathExpressionResolver.resolve(
-            context,
-            Arrays.asList("name1", "<.marvel-{now/d}>", "name2", "<.logstash-{now/M{uuuu.MM}}>")
-        );
-        assertThat(result.size(), equalTo(4));
-        assertThat(result.get(0), equalTo("name1"));
-        assertThat(result.get(1), equalTo(".marvel-" + formatDate("uuuu.MM.dd", dateFromMillis(context.getStartTime()))));
-        assertThat(result.get(2), equalTo("name2"));
-        assertThat(result.get(3), equalTo(".logstash-" + formatDate("uuuu.MM", dateFromMillis(context.getStartTime()).withDayOfMonth(1))));
+    public void testExpression_EscapeDateFormat() {
+        String result = DateMathExpressionResolver.resolveExpression("<.marvel-{now/d{'\\{year\\}'yyyy}}>", getTime);
+        assertThat(result, equalTo(".marvel-" + formatDate("'{year}'yyyy", dateFromMillis(now))));
     }
 
-    public void testExpression_CustomTimeZoneInIndexName() throws Exception {
+    public void testExpression_CustomTimeZoneInIndexName() {
         ZoneId timeZone;
         int hoursOffset;
         int minutesOffset = 0;
@@ -194,57 +121,57 @@ public class DateMathExpressionResolverTests extends ESTestCase {
             // rounding to today 00:00
             now = ZonedDateTime.now(ZoneOffset.UTC).withHour(0).withMinute(0).withSecond(0);
         }
-        Context context = new Context(
-            this.context.getState(),
-            this.context.getOptions(),
-            now.toInstant().toEpochMilli(),
-            SystemIndexAccessLevel.NONE,
-            name -> false,
-            name -> false
-        );
-        List<String> results = DateMathExpressionResolver.resolve(
-            context,
-            Arrays.asList("<.marvel-{now/d{yyyy.MM.dd|" + timeZone.getId() + "}}>")
+
+        String result = DateMathExpressionResolver.resolveExpression(
+            "<.marvel-{now/d{yyyy.MM.dd|" + timeZone.getId() + "}}>",
+            () -> now.toInstant().toEpochMilli()
         );
-        assertThat(results.size(), equalTo(1));
-        logger.info("timezone: [{}], now [{}], name: [{}]", timeZone, now, results.get(0));
-        assertThat(results.get(0), equalTo(".marvel-" + formatDate("uuuu.MM.dd", now.withZoneSameInstant(timeZone))));
+        logger.info("timezone: [{}], now [{}], name: [{}]", timeZone, now, result);
+        assertThat(result, equalTo(".marvel-" + formatDate("uuuu.MM.dd", now.withZoneSameInstant(timeZone))));
     }
 
-    public void testExpressionInvalidUnescaped() throws Exception {
+    public void testExpressionInvalidUnescaped() {
         Exception e = expectThrows(
             ElasticsearchParseException.class,
-            () -> DateMathExpressionResolver.resolve(context, Arrays.asList("<.mar}vel-{now/d}>"))
+            () -> DateMathExpressionResolver.resolveExpression("<.mar}vel-{now/d}>", getTime)
         );
         assertThat(e.getMessage(), containsString("invalid dynamic name expression"));
         assertThat(e.getMessage(), containsString("invalid character at position ["));
     }
 
-    public void testExpressionInvalidDateMathFormat() throws Exception {
+    public void testExpressionInvalidDateMathFormat() {
         Exception e = expectThrows(
             ElasticsearchParseException.class,
-            () -> DateMathExpressionResolver.resolve(context, Arrays.asList("<.marvel-{now/d{}>"))
+            () -> DateMathExpressionResolver.resolveExpression("<.marvel-{now/d{}>", getTime)
         );
         assertThat(e.getMessage(), containsString("invalid dynamic name expression"));
         assertThat(e.getMessage(), containsString("date math placeholder is open ended"));
     }
 
-    public void testExpressionInvalidEmptyDateMathFormat() throws Exception {
+    public void testExpressionInvalidEmptyDateMathFormat() {
         Exception e = expectThrows(
             ElasticsearchParseException.class,
-            () -> DateMathExpressionResolver.resolve(context, Arrays.asList("<.marvel-{now/d{}}>"))
+            () -> DateMathExpressionResolver.resolveExpression("<.marvel-{now/d{}}>", getTime)
         );
         assertThat(e.getMessage(), containsString("invalid dynamic name expression"));
         assertThat(e.getMessage(), containsString("missing date format"));
     }
 
-    public void testExpressionInvalidOpenEnded() throws Exception {
+    public void testExpressionInvalidOpenEnded() {
         Exception e = expectThrows(
             ElasticsearchParseException.class,
-            () -> DateMathExpressionResolver.resolve(context, Arrays.asList("<.marvel-{now/d>"))
+            () -> DateMathExpressionResolver.resolveExpression("<.marvel-{now/d>", getTime)
         );
         assertThat(e.getMessage(), containsString("invalid dynamic name expression"));
         assertThat(e.getMessage(), containsString("date math placeholder is open ended"));
     }
 
+    static ZonedDateTime dateFromMillis(long millis) {
+        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.UTC);
+    }
+
+    static String formatDate(String pattern, ZonedDateTime zonedDateTime) {
+        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(pattern, Locale.ROOT);
+        return dateFormatter.format(zonedDateTime);
+    }
 }

+ 105 - 77
server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java

@@ -25,6 +25,7 @@ import org.elasticsearch.cluster.metadata.IndexMetadata.State;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.core.Predicates;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.index.Index;
 import org.elasticsearch.index.IndexNotFoundException;
@@ -47,6 +48,7 @@ import java.time.Instant;
 import java.time.LocalDate;
 import java.time.ZoneOffset;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -58,6 +60,8 @@ import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.backingInd
 import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.createBackingIndex;
 import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.createFailureStore;
 import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.newInstance;
+import static org.elasticsearch.cluster.metadata.DateMathExpressionResolverTests.dateFromMillis;
+import static org.elasticsearch.cluster.metadata.DateMathExpressionResolverTests.formatDate;
 import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_HIDDEN_SETTING;
 import static org.elasticsearch.common.util.set.Sets.newHashSet;
 import static org.elasticsearch.indices.SystemIndices.EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY;
@@ -885,10 +889,7 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
             IndicesOptions.lenientExpandOpen(),
             SystemIndexAccessLevel.NONE
         );
-        assertThat(
-            newHashSet(indexNameExpressionResolver.concreteIndexNames(context, new String[] {})),
-            equalTo(newHashSet("kuku", "testXXX"))
-        );
+        assertThat(newHashSet(indexNameExpressionResolver.concreteIndexNames(context)), equalTo(newHashSet("kuku", "testXXX")));
     }
 
     public void testConcreteIndicesNoIndicesErrorMessage() {
@@ -1408,52 +1409,56 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
         }
     }
 
-    public void testIsAllIndicesNull() throws Exception {
+    public void testIsAllIndicesNull() {
         assertThat(IndexNameExpressionResolver.isAllIndices(null), equalTo(true));
     }
 
-    public void testIsAllIndicesEmpty() throws Exception {
-        assertThat(IndexNameExpressionResolver.isAllIndices(Collections.<String>emptyList()), equalTo(true));
+    public void testIsAllIndicesEmpty() {
+        assertThat(IndexNameExpressionResolver.isAllIndices(List.of()), equalTo(true));
+    }
+
+    public void testIsAllIndicesExplicitAll() {
+        assertThat(IndexNameExpressionResolver.isAllIndices(List.of("_all")), equalTo(true));
     }
 
-    public void testIsAllIndicesExplicitAll() throws Exception {
-        assertThat(IndexNameExpressionResolver.isAllIndices(Arrays.asList("_all")), equalTo(true));
+    public void testIsAllIndicesExplicitAllPlusOther() {
+        assertThat(IndexNameExpressionResolver.isAllIndices(List.of("_all", "other")), equalTo(false));
     }
 
-    public void testIsAllIndicesExplicitAllPlusOther() throws Exception {
-        assertThat(IndexNameExpressionResolver.isAllIndices(Arrays.asList("_all", "other")), equalTo(false));
+    public void testIsNoneIndices() {
+        assertThat(IndexNameExpressionResolver.isNoneExpression(new String[] { "*", "-*" }), equalTo(true));
     }
 
-    public void testIsAllIndicesNormalIndexes() throws Exception {
-        assertThat(IndexNameExpressionResolver.isAllIndices(Arrays.asList("index1", "index2", "index3")), equalTo(false));
+    public void testIsAllIndicesNormalIndexes() {
+        assertThat(IndexNameExpressionResolver.isAllIndices(List.of("index1", "index2", "index3")), equalTo(false));
     }
 
-    public void testIsAllIndicesWildcard() throws Exception {
-        assertThat(IndexNameExpressionResolver.isAllIndices(Arrays.asList("*")), equalTo(false));
+    public void testIsAllIndicesWildcard() {
+        assertThat(IndexNameExpressionResolver.isAllIndices(List.of("*")), equalTo(false));
     }
 
-    public void testIsExplicitAllIndicesNull() throws Exception {
+    public void testIsExplicitAllIndicesNull() {
         assertThat(IndexNameExpressionResolver.isExplicitAllPattern(null), equalTo(false));
     }
 
-    public void testIsExplicitAllIndicesEmpty() throws Exception {
-        assertThat(IndexNameExpressionResolver.isExplicitAllPattern(Collections.<String>emptyList()), equalTo(false));
+    public void testIsExplicitAllIndicesEmpty() {
+        assertThat(IndexNameExpressionResolver.isExplicitAllPattern(List.of()), equalTo(false));
     }
 
-    public void testIsExplicitAllIndicesExplicitAll() throws Exception {
-        assertThat(IndexNameExpressionResolver.isExplicitAllPattern(Arrays.asList("_all")), equalTo(true));
+    public void testIsExplicitAllIndicesExplicitAll() {
+        assertThat(IndexNameExpressionResolver.isExplicitAllPattern(List.of("_all")), equalTo(true));
     }
 
-    public void testIsExplicitAllIndicesExplicitAllPlusOther() throws Exception {
-        assertThat(IndexNameExpressionResolver.isExplicitAllPattern(Arrays.asList("_all", "other")), equalTo(false));
+    public void testIsExplicitAllIndicesExplicitAllPlusOther() {
+        assertThat(IndexNameExpressionResolver.isExplicitAllPattern(List.of("_all", "other")), equalTo(false));
     }
 
-    public void testIsExplicitAllIndicesNormalIndexes() throws Exception {
-        assertThat(IndexNameExpressionResolver.isExplicitAllPattern(Arrays.asList("index1", "index2", "index3")), equalTo(false));
+    public void testIsExplicitAllIndicesNormalIndexes() {
+        assertThat(IndexNameExpressionResolver.isExplicitAllPattern(List.of("index1", "index2", "index3")), equalTo(false));
     }
 
-    public void testIsExplicitAllIndicesWildcard() throws Exception {
-        assertThat(IndexNameExpressionResolver.isExplicitAllPattern(Arrays.asList("*")), equalTo(false));
+    public void testIsExplicitAllIndicesWildcard() {
+        assertThat(IndexNameExpressionResolver.isExplicitAllPattern(List.of("*")), equalTo(false));
     }
 
     public void testIndexOptionsFailClosedIndicesAndAliases() {
@@ -1580,16 +1585,13 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
             .put(indexBuilder("test-1").state(State.OPEN).putAlias(AliasMetadata.builder("alias-1")));
         ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build();
 
-        assertEquals(new HashSet<>(Arrays.asList("alias-0", "alias-1")), indexNameExpressionResolver.resolveExpressions(state, "alias-*"));
+        assertEquals(Set.of("alias-0", "alias-1"), indexNameExpressionResolver.resolveExpressions(state, "alias-*"));
+        assertEquals(Set.of("test-0", "alias-0", "alias-1"), indexNameExpressionResolver.resolveExpressions(state, "test-0", "alias-*"));
         assertEquals(
-            new HashSet<>(Arrays.asList("test-0", "alias-0", "alias-1")),
-            indexNameExpressionResolver.resolveExpressions(state, "test-0", "alias-*")
-        );
-        assertEquals(
-            new HashSet<>(Arrays.asList("test-0", "test-1", "alias-0", "alias-1")),
+            Set.of("test-0", "test-1", "alias-0", "alias-1"),
             indexNameExpressionResolver.resolveExpressions(state, "test-*", "alias-*")
         );
-        assertEquals(new HashSet<>(Arrays.asList("test-1", "alias-1")), indexNameExpressionResolver.resolveExpressions(state, "*-1"));
+        assertEquals(Set.of("test-1", "alias-1"), indexNameExpressionResolver.resolveExpressions(state, "*-1"));
     }
 
     public void testFilteringAliases() {
@@ -1598,16 +1600,16 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
             .put(indexBuilder("test-1").state(State.OPEN).putAlias(AliasMetadata.builder("alias-1")));
         ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build();
 
-        Set<String> resolvedExpressions = new HashSet<>(Arrays.asList("alias-0", "alias-1"));
+        Set<String> resolvedExpressions = Set.of("alias-0", "alias-1");
         String[] strings = indexNameExpressionResolver.filteringAliases(state, "test-0", resolvedExpressions);
         assertArrayEquals(new String[] { "alias-0" }, strings);
 
         // concrete index supersedes filtering alias
-        resolvedExpressions = new HashSet<>(Arrays.asList("test-0", "alias-0", "alias-1"));
+        resolvedExpressions = Set.of("test-0", "alias-0", "alias-1");
         strings = indexNameExpressionResolver.filteringAliases(state, "test-0", resolvedExpressions);
         assertNull(strings);
 
-        resolvedExpressions = new HashSet<>(Arrays.asList("test-0", "test-1", "alias-0", "alias-1"));
+        resolvedExpressions = Set.of("test-0", "test-1", "alias-0", "alias-1");
         strings = indexNameExpressionResolver.filteringAliases(state, "test-0", resolvedExpressions);
         assertNull(strings);
     }
@@ -1742,7 +1744,7 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
             );
         ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build();
 
-        Set<String> resolvedExpressions = new HashSet<>(Arrays.asList("test-0", "test-alias"));
+        Set<String> resolvedExpressions = Set.of("test-0", "test-alias");
         String[] aliases = indexNameExpressionResolver.indexAliases(state, "test-0", x -> true, x -> true, false, resolvedExpressions);
         assertNull(aliases);
         aliases = indexNameExpressionResolver.indexAliases(state, "test-0", x -> true, x -> true, true, resolvedExpressions);
@@ -1769,7 +1771,7 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
             x -> true,
             x -> true,
             true,
-            new HashSet<>(Arrays.asList("test-0", "test-alias"))
+            Set.of("test-0", "test-alias")
         );
         Arrays.sort(strings);
         assertArrayEquals(new String[] { "test-alias" }, strings);
@@ -1851,7 +1853,7 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
             x -> true,
             x -> true,
             true,
-            new HashSet<>(Arrays.asList("test-0", "test-1", "test-alias"))
+            Set.of("test-0", "test-1", "test-alias")
         );
         Arrays.sort(strings);
         assertArrayEquals(new String[] { "test-alias" }, strings);
@@ -1889,7 +1891,7 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
             x -> true,
             x -> true,
             true,
-            new HashSet<>(Arrays.asList("test-0", "test-alias"))
+            Set.of("test-0", "test-alias")
         );
         Arrays.sort(strings);
         assertArrayEquals(new String[] { "test-alias" }, strings);
@@ -1925,7 +1927,7 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
             x -> true,
             x -> true,
             true,
-            new HashSet<>(Arrays.asList("test-0", "test-1", "test-alias"))
+            Set.of("test-0", "test-1", "test-alias")
         );
         Arrays.sort(strings);
         assertArrayEquals(new String[] { "test-alias" }, strings);
@@ -1966,7 +1968,7 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
             x -> true,
             x -> true,
             true,
-            new HashSet<>(Arrays.asList("test-0", "test-1", "test-alias"))
+            Set.of("test-0", "test-1", "test-alias")
         );
         Arrays.sort(strings);
         assertArrayEquals(new String[] { "test-alias" }, strings);
@@ -2328,40 +2330,40 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
         SearchRequest request = new SearchRequest(randomFrom("*", "_all"));
         request.indicesOptions(IndicesOptions.strictExpandHidden());
 
-        List<String> indexNames = resolveConcreteIndexNameList(state, request);
-        assertThat(indexNames, containsInAnyOrder("some-other-index", ".ml-stuff", ".ml-meta", ".watches"));
+        String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+        assertThat(indexNames, arrayContainingInAnyOrder("some-other-index", ".ml-stuff", ".ml-meta", ".watches"));
     }
 
     public void testWildcardSystemIndexResolutionMultipleMatchesAllowed() {
         ClusterState state = systemIndexTestClusterState();
         SearchRequest request = new SearchRequest(".w*");
 
-        List<String> indexNames = resolveConcreteIndexNameList(state, request);
-        assertThat(indexNames, containsInAnyOrder(".watches"));
+        String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+        assertThat(indexNames, arrayContainingInAnyOrder(".watches"));
     }
 
     public void testWildcardSystemIndexResolutionSingleMatchAllowed() {
         ClusterState state = systemIndexTestClusterState();
         SearchRequest request = new SearchRequest(".ml-*");
 
-        List<String> indexNames = resolveConcreteIndexNameList(state, request);
-        assertThat(indexNames, containsInAnyOrder(".ml-meta", ".ml-stuff"));
+        String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+        assertThat(indexNames, arrayContainingInAnyOrder(".ml-meta", ".ml-stuff"));
     }
 
     public void testSingleSystemIndexResolutionAllowed() {
         ClusterState state = systemIndexTestClusterState();
         SearchRequest request = new SearchRequest(".ml-meta");
 
-        List<String> indexNames = resolveConcreteIndexNameList(state, request);
-        assertThat(indexNames, containsInAnyOrder(".ml-meta"));
+        String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+        assertThat(indexNames, arrayContainingInAnyOrder(".ml-meta"));
     }
 
     public void testFullWildcardSystemIndicesAreHidden() {
         ClusterState state = systemIndexTestClusterState();
         SearchRequest request = new SearchRequest(randomFrom("*", "_all"));
 
-        List<String> indexNames = resolveConcreteIndexNameList(state, request);
-        assertThat(indexNames, containsInAnyOrder("some-other-index"));
+        String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+        assertThat(indexNames, arrayContaining("some-other-index"));
     }
 
     public void testFullWildcardSystemIndexResolutionDeprecated() {
@@ -2370,8 +2372,8 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
         SearchRequest request = new SearchRequest(randomFrom("*", "_all"));
         request.indicesOptions(IndicesOptions.strictExpandHidden());
 
-        List<String> indexNames = resolveConcreteIndexNameList(state, request);
-        assertThat(indexNames, containsInAnyOrder("some-other-index", ".ml-stuff", ".ml-meta", ".watches"));
+        String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+        assertThat(indexNames, arrayContainingInAnyOrder("some-other-index", ".ml-stuff", ".ml-meta", ".watches"));
         assertWarnings(
             true,
             new DeprecationWarning(
@@ -2388,8 +2390,8 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
         ClusterState state = systemIndexTestClusterState();
         SearchRequest request = new SearchRequest(".ml-meta");
 
-        List<String> indexNames = resolveConcreteIndexNameList(state, request);
-        assertThat(indexNames, containsInAnyOrder(".ml-meta"));
+        String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+        assertThat(indexNames, arrayContaining(".ml-meta"));
         assertWarnings(
             true,
             new DeprecationWarning(
@@ -2405,8 +2407,8 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
         ClusterState state = systemIndexTestClusterState();
         SearchRequest request = new SearchRequest(".w*");
 
-        List<String> indexNames = resolveConcreteIndexNameList(state, request);
-        assertThat(indexNames, containsInAnyOrder(".watches"));
+        String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+        assertThat(indexNames, arrayContainingInAnyOrder(".watches"));
         assertWarnings(
             true,
             new DeprecationWarning(
@@ -2423,8 +2425,8 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
         ClusterState state = systemIndexTestClusterState();
         SearchRequest request = new SearchRequest(".ml-*");
 
-        List<String> indexNames = resolveConcreteIndexNameList(state, request);
-        assertThat(indexNames, containsInAnyOrder(".ml-meta", ".ml-stuff"));
+        String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+        assertThat(indexNames, arrayContainingInAnyOrder(".ml-meta", ".ml-stuff"));
         assertWarnings(
             true,
             new DeprecationWarning(
@@ -2479,8 +2481,8 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
                 threadContext.putHeader(SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, Boolean.FALSE.toString());
                 SearchRequest request = new SearchRequest(".external-*");
 
-                List<String> indexNames = resolveConcreteIndexNameList(state, request);
-                assertThat(indexNames, contains(".external-sys-idx"));
+                String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+                assertThat(indexNames, arrayContaining(".external-sys-idx"));
                 assertWarnings(
                     true,
                     new DeprecationWarning(
@@ -2496,8 +2498,8 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
                 threadContext.putHeader(SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, Boolean.FALSE.toString());
                 SearchRequest request = new SearchRequest(".external-sys-idx");
 
-                List<String> indexNames = resolveConcreteIndexNameList(state, request);
-                assertThat(indexNames, contains(".external-sys-idx"));
+                String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+                assertThat(indexNames, arrayContaining(".external-sys-idx"));
                 assertWarnings(
                     true,
                     new DeprecationWarning(
@@ -2515,8 +2517,8 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
                 threadContext.putHeader(EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, "stack-component");
                 SearchRequest request = new SearchRequest(".external-*");
 
-                List<String> indexNames = resolveConcreteIndexNameList(state, request);
-                assertThat(indexNames, contains(".external-sys-idx"));
+                String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+                assertThat(indexNames, arrayContaining(".external-sys-idx"));
                 assertWarnings();
             }
         }
@@ -2526,8 +2528,8 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
                 threadContext.putHeader(EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, "stack-component");
                 SearchRequest request = new SearchRequest(".external-sys-idx");
 
-                List<String> indexNames = resolveConcreteIndexNameList(state, request);
-                assertThat(indexNames, contains(".external-sys-idx"));
+                String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+                assertThat(indexNames, arrayContaining(".external-sys-idx"));
                 assertWarnings();
             }
         }
@@ -2538,8 +2540,8 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
                 threadContext.putHeader(EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, "other");
                 SearchRequest request = new SearchRequest(".external-*");
 
-                List<String> indexNames = resolveConcreteIndexNameList(state, request);
-                assertThat(indexNames, contains(".external-sys-idx"));
+                String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+                assertThat(indexNames, arrayContaining(".external-sys-idx"));
                 assertWarnings();
             }
         }
@@ -2549,8 +2551,8 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
                 threadContext.putHeader(EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, "other");
                 SearchRequest request = new SearchRequest(".external-sys-idx");
 
-                List<String> indexNames = resolveConcreteIndexNameList(state, request);
-                assertThat(indexNames, contains(".external-sys-idx"));
+                String[] indexNames = indexNameExpressionResolver.concreteIndexNames(state, request);
+                assertThat(indexNames, arrayContaining(".external-sys-idx"));
                 assertWarnings();
             }
         }
@@ -3073,7 +3075,6 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
             assertThat(result[1].getName(), equalTo(DataStream.getDefaultBackingIndexName(dataStream1, 2, epochMillis)));
             assertThat(result[2].getName(), equalTo(DataStream.getDefaultBackingIndexName(dataStream2, 1, epochMillis)));
             assertThat(result[3].getName(), equalTo(DataStream.getDefaultBackingIndexName(dataStream2, 2, epochMillis)));
-            ;
         }
         {
             IndicesOptions indicesOptions = IndicesOptions.STRICT_EXPAND_OPEN;
@@ -3239,6 +3240,37 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
         assertThat(names, empty());
     }
 
+    public void testDateMathMixedArray() {
+        long now = System.currentTimeMillis();
+        String dataMathIndex1 = ".marvel-" + formatDate("uuuu.MM.dd", dateFromMillis(now));
+        String dateMathIndex2 = ".logstash-" + formatDate("uuuu.MM", dateFromMillis(now).withDayOfMonth(1));
+        IndexNameExpressionResolver.Context context = new IndexNameExpressionResolver.Context(
+            ClusterState.builder(new ClusterName("_name"))
+                .metadata(
+                    Metadata.builder()
+                        .put(indexBuilder("name1"))
+                        .put(indexBuilder("name2"))
+                        .put(indexBuilder(dataMathIndex1))
+                        .put(indexBuilder(dateMathIndex2))
+                )
+                .build(),
+            IndicesOptions.strictExpand(),
+            now,
+            SystemIndexAccessLevel.NONE,
+            Predicates.never(),
+            Predicates.never()
+        );
+        Collection<String> result = IndexNameExpressionResolver.resolveExpressionsToResources(
+            context,
+            "name1",
+            "<.marvel-{now/d}>",
+            "name2",
+            "<.logstash-{now/M{uuuu.MM}}>"
+        );
+        assertThat(result.size(), equalTo(4));
+        assertThat(result, contains("name1", dataMathIndex1, "name2", dateMathIndex2));
+    }
+
     public void testMathExpressionSupport() {
         Instant instant = LocalDate.of(2021, 01, 11).atStartOfDay().toInstant(ZoneOffset.UTC);
         String resolved = IndexNameExpressionResolver.resolveDateMathExpression("<a-name-{now/M{yyyy-MM}}>", instant.toEpochMilli());
@@ -3418,10 +3450,6 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
         return ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build();
     }
 
-    private List<String> resolveConcreteIndexNameList(ClusterState state, SearchRequest request) {
-        return Arrays.stream(indexNameExpressionResolver.concreteIndices(state, request)).map(Index::getName).toList();
-    }
-
     private static IndexMetadata.Builder indexBuilder(String index, Settings additionalSettings) {
         return IndexMetadata.builder(index).settings(indexSettings(IndexVersion.current(), 1, 0).put(additionalSettings));
     }

+ 55 - 251
server/src/test/java/org/elasticsearch/cluster/metadata/WildcardExpressionResolverTests.java

@@ -13,23 +13,20 @@ import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.IndexMetadata.State;
-import org.elasticsearch.index.IndexNotFoundException;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.indices.SystemIndices.SystemIndexAccessLevel;
 import org.elasticsearch.test.ESTestCase;
 
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.function.Predicate;
 
 import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.createBackingIndex;
 import static org.elasticsearch.common.util.set.Sets.newHashSet;
 import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.is;
 
 public class WildcardExpressionResolverTests extends ESTestCase {
 
@@ -50,107 +47,31 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             SystemIndexAccessLevel.NONE
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("testXXX"))),
-            equalTo(newHashSet("testXXX"))
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "ku*")),
+            equalTo(newHashSet("kuku"))
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Arrays.asList("testXXX", "testYYY"))),
-            equalTo(newHashSet("testXXX", "testYYY"))
-        );
-        assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Arrays.asList("testXXX", "ku*"))),
-            equalTo(newHashSet("testXXX", "kuku"))
-        );
-        assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("test*"))),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "test*")),
             equalTo(newHashSet("testXXX", "testXYY", "testYYY"))
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("testX*"))),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "testX*")),
             equalTo(newHashSet("testXXX", "testXYY"))
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Arrays.asList("testX*", "kuku"))),
-            equalTo(newHashSet("testXXX", "testXYY", "kuku"))
-        );
-        assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("*"))),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "*")),
             equalTo(newHashSet("testXXX", "testXYY", "testYYY", "kuku"))
         );
-        assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Arrays.asList("*", "-kuku"))),
-            equalTo(newHashSet("testXXX", "testXYY", "testYYY"))
-        );
-        assertThat(
-            newHashSet(
-                IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
-                    context,
-                    Arrays.asList("testX*", "-doe", "-testXXX", "-testYYY")
-                )
-            ),
-            equalTo(newHashSet("testXYY"))
-        );
-        if (indicesOptions == IndicesOptions.lenientExpandOpen()) {
-            assertThat(
-                newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Arrays.asList("testXXX", "-testXXX"))),
-                equalTo(newHashSet("testXXX", "-testXXX"))
-            );
-        } else if (indicesOptions == IndicesOptions.strictExpandOpen()) {
-            IndexNotFoundException infe = expectThrows(
-                IndexNotFoundException.class,
-                () -> IndexNameExpressionResolver.resolveExpressions(context, "testXXX", "-testXXX")
-            );
-            assertEquals("-testXXX", infe.getIndex().getName());
-        }
-        assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Arrays.asList("testXXX", "-testX*"))),
-            equalTo(newHashSet("testXXX"))
-        );
-    }
-
-    public void testConvertWildcardsTests() {
-        Metadata.Builder mdBuilder = Metadata.builder()
-            .put(indexBuilder("testXXX").putAlias(AliasMetadata.builder("alias1")).putAlias(AliasMetadata.builder("alias2")))
-            .put(indexBuilder("testXYY").putAlias(AliasMetadata.builder("alias2")))
-            .put(indexBuilder("testYYY").putAlias(AliasMetadata.builder("alias3")))
-            .put(indexBuilder("kuku"));
-        ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build();
-
-        IndexNameExpressionResolver.Context context = new IndexNameExpressionResolver.Context(
-            state,
-            IndicesOptions.lenientExpandOpen(),
-            SystemIndexAccessLevel.NONE
-        );
-        assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Arrays.asList("testYY*", "alias*"))),
-            equalTo(newHashSet("testXXX", "testXYY", "testYYY"))
-        );
-        assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("-kuku"))),
-            equalTo(newHashSet("-kuku"))
-        );
-        assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Arrays.asList("test*", "-testYYY"))),
-            equalTo(newHashSet("testXXX", "testXYY"))
-        );
-        assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Arrays.asList("testX*", "testYYY"))),
-            equalTo(newHashSet("testXXX", "testXYY", "testYYY"))
-        );
-        assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Arrays.asList("testYYY", "testX*"))),
-            equalTo(newHashSet("testXXX", "testXYY", "testYYY"))
-        );
     }
 
     public void testConvertWildcardsOpenClosedIndicesTests() {
         Metadata.Builder mdBuilder = Metadata.builder()
-            .put(indexBuilder("testXXX").state(IndexMetadata.State.OPEN))
-            .put(indexBuilder("testXXY").state(IndexMetadata.State.OPEN))
-            .put(indexBuilder("testXYY").state(IndexMetadata.State.CLOSE))
-            .put(indexBuilder("testYYY").state(IndexMetadata.State.OPEN))
-            .put(indexBuilder("testYYX").state(IndexMetadata.State.CLOSE))
-            .put(indexBuilder("kuku").state(IndexMetadata.State.OPEN));
+            .put(indexBuilder("testXXX").state(State.OPEN))
+            .put(indexBuilder("testXXY").state(State.OPEN))
+            .put(indexBuilder("testXYY").state(State.CLOSE))
+            .put(indexBuilder("testYYY").state(State.OPEN))
+            .put(indexBuilder("testYYX").state(State.CLOSE))
+            .put(indexBuilder("kuku").state(State.OPEN));
         ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build();
 
         IndexNameExpressionResolver.Context context = new IndexNameExpressionResolver.Context(
@@ -159,7 +80,7 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             SystemIndexAccessLevel.NONE
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("testX*"))),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "testX*")),
             equalTo(newHashSet("testXXX", "testXXY", "testXYY"))
         );
         context = new IndexNameExpressionResolver.Context(
@@ -168,7 +89,7 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             SystemIndexAccessLevel.NONE
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("testX*"))),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "testX*")),
             equalTo(newHashSet("testXYY"))
         );
         context = new IndexNameExpressionResolver.Context(
@@ -177,26 +98,9 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             SystemIndexAccessLevel.NONE
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("testX*"))),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "testX*")),
             equalTo(newHashSet("testXXX", "testXXY"))
         );
-        context = new IndexNameExpressionResolver.Context(
-            state,
-            IndicesOptions.fromOptions(true, true, false, false),
-            SystemIndexAccessLevel.NONE
-        );
-        assertThat(IndexNameExpressionResolver.resolveExpressions(context, "testX*").size(), equalTo(0));
-        context = new IndexNameExpressionResolver.Context(
-            state,
-            IndicesOptions.fromOptions(false, true, false, false),
-            SystemIndexAccessLevel.NONE
-        );
-        IndexNameExpressionResolver.Context finalContext = context;
-        IndexNotFoundException infe = expectThrows(
-            IndexNotFoundException.class,
-            () -> IndexNameExpressionResolver.resolveExpressions(finalContext, "testX*")
-        );
-        assertThat(infe.getIndex().getName(), is("testX*"));
     }
 
     // issue #13334
@@ -217,28 +121,27 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             SystemIndexAccessLevel.NONE
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("test*X*"))),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "test*X*")),
             equalTo(newHashSet("testXXX", "testXXY", "testXYY"))
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("test*X*Y"))),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "test*X*Y")),
             equalTo(newHashSet("testXXY", "testXYY"))
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("kuku*Y*"))),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "kuku*Y*")),
             equalTo(newHashSet("kukuYYY"))
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("*Y*"))),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "*Y*")),
             equalTo(newHashSet("testXXY", "testXYY", "testYYY", "kukuYYY"))
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("test*Y*X")))
-                .size(),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "test*Y*X")).size(),
             equalTo(0)
         );
         assertThat(
-            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("*Y*X"))).size(),
+            newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "*Y*X")).size(),
             equalTo(0)
         );
     }
@@ -259,26 +162,6 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolveAll(context)),
             equalTo(newHashSet("testXXX", "testXYY", "testYYY"))
         );
-        assertThat(
-            newHashSet(IndexNameExpressionResolver.resolveExpressions(context, "_all")),
-            equalTo(newHashSet("testXXX", "testXYY", "testYYY"))
-        );
-        IndicesOptions noExpandOptions = IndicesOptions.fromOptions(
-            randomBoolean(),
-            true,
-            false,
-            false,
-            randomBoolean(),
-            randomBoolean(),
-            randomBoolean(),
-            randomBoolean()
-        );
-        IndexNameExpressionResolver.Context noExpandContext = new IndexNameExpressionResolver.Context(
-            state,
-            noExpandOptions,
-            SystemIndexAccessLevel.NONE
-        );
-        assertThat(IndexNameExpressionResolver.resolveExpressions(noExpandContext, "_all").size(), equalTo(0));
     }
 
     public void testAllAliases() {
@@ -506,112 +389,47 @@ public class WildcardExpressionResolverTests extends ESTestCase {
         );
 
         {
-            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
+            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
                 indicesAndAliasesContext,
-                Collections.singletonList("foo_a*")
+                "foo_a*"
             );
             assertThat(indices, containsInAnyOrder("foo_index", "bar_index"));
         }
         {
-            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
+            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
                 skipAliasesLenientContext,
-                Collections.singletonList("foo_a*")
+                "foo_a*"
             );
             assertEquals(0, indices.size());
         }
         {
-            IndexNotFoundException infe = expectThrows(
-                IndexNotFoundException.class,
-                () -> IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
-                    skipAliasesStrictContext,
-                    Collections.singletonList("foo_a*")
-                )
+            Set<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
+                skipAliasesStrictContext,
+                "foo_a*"
             );
-            assertEquals("foo_a*", infe.getIndex().getName());
+            assertThat(indices, empty());
         }
         {
-            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
+            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
                 indicesAndAliasesContext,
-                Collections.singletonList("foo*")
+                "foo*"
             );
             assertThat(indices, containsInAnyOrder("foo_foo", "foo_index", "bar_index"));
         }
         {
-            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
+            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
                 skipAliasesLenientContext,
-                Collections.singletonList("foo*")
+                "foo*"
             );
             assertThat(indices, containsInAnyOrder("foo_foo", "foo_index"));
         }
         {
-            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
+            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
                 skipAliasesStrictContext,
-                Collections.singletonList("foo*")
+                "foo*"
             );
             assertThat(indices, containsInAnyOrder("foo_foo", "foo_index"));
         }
-        {
-            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
-                indicesAndAliasesContext,
-                Collections.singletonList("foo_alias")
-            );
-            assertThat(indices, containsInAnyOrder("foo_alias"));
-        }
-        {
-            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
-                skipAliasesLenientContext,
-                Collections.singletonList("foo_alias")
-            );
-            assertThat(indices, containsInAnyOrder("foo_alias"));
-        }
-        {
-            IllegalArgumentException iae = expectThrows(
-                IllegalArgumentException.class,
-                () -> IndexNameExpressionResolver.resolveExpressions(skipAliasesStrictContext, "foo_alias")
-            );
-            assertEquals(
-                "The provided expression [foo_alias] matches an alias, specify the corresponding concrete indices instead.",
-                iae.getMessage()
-            );
-        }
-        IndicesOptions noExpandNoAliasesIndicesOptions = IndicesOptions.fromOptions(true, false, false, false, true, false, true, false);
-        IndexNameExpressionResolver.Context noExpandNoAliasesContext = new IndexNameExpressionResolver.Context(
-            state,
-            noExpandNoAliasesIndicesOptions,
-            SystemIndexAccessLevel.NONE
-        );
-        {
-            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
-                noExpandNoAliasesContext,
-                List.of("foo_alias")
-            );
-            assertThat(indices, containsInAnyOrder("foo_alias"));
-        }
-        IndicesOptions strictNoExpandNoAliasesIndicesOptions = IndicesOptions.fromOptions(
-            false,
-            true,
-            false,
-            false,
-            true,
-            false,
-            true,
-            false
-        );
-        IndexNameExpressionResolver.Context strictNoExpandNoAliasesContext = new IndexNameExpressionResolver.Context(
-            state,
-            strictNoExpandNoAliasesIndicesOptions,
-            SystemIndexAccessLevel.NONE
-        );
-        {
-            IllegalArgumentException iae = expectThrows(
-                IllegalArgumentException.class,
-                () -> IndexNameExpressionResolver.resolveExpressions(strictNoExpandNoAliasesContext, "foo_alias")
-            );
-            assertEquals(
-                "The provided expression [foo_alias] matches an alias, specify the corresponding concrete indices instead.",
-                iae.getMessage()
-            );
-        }
     }
 
     public void testResolveDataStreams() {
@@ -654,17 +472,14 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             );
 
             // data streams are not included but expression matches the data stream
-            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
+            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
                 indicesAndAliasesContext,
-                Collections.singletonList("foo_*")
+                "foo_*"
             );
             assertThat(indices, containsInAnyOrder("foo_index", "foo_foo", "bar_index"));
 
             // data streams are not included and expression doesn't match the data steram
-            indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
-                indicesAndAliasesContext,
-                Collections.singletonList("bar_*")
-            );
+            indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(indicesAndAliasesContext, "bar_*");
             assertThat(indices, containsInAnyOrder("bar_bar", "bar_index"));
         }
 
@@ -691,9 +506,9 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             );
 
             // data stream's corresponding backing indices are resolved
-            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
+            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
                 indicesAliasesAndDataStreamsContext,
-                Collections.singletonList("foo_*")
+                "foo_*"
             );
             assertThat(
                 indices,
@@ -707,9 +522,9 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             );
 
             // include all wildcard adds the data stream's backing indices
-            indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
+            indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
                 indicesAliasesAndDataStreamsContext,
-                Collections.singletonList("*")
+                "*"
             );
             assertThat(
                 indices,
@@ -748,9 +563,9 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             );
 
             // data stream's corresponding backing indices are resolved
-            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
+            Collection<String> indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
                 indicesAliasesDataStreamsAndHiddenIndices,
-                Collections.singletonList("foo_*")
+                "foo_*"
             );
             assertThat(
                 indices,
@@ -764,9 +579,9 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             );
 
             // include all wildcard adds the data stream's backing indices
-            indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(
+            indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
                 indicesAliasesDataStreamsAndHiddenIndices,
-                Collections.singletonList("*")
+                "*"
             );
             assertThat(
                 indices,
@@ -808,24 +623,17 @@ public class WildcardExpressionResolverTests extends ESTestCase {
             SystemIndexAccessLevel.NONE
         );
 
-        Collection<String> matches = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(indicesAndAliasesContext, List.of("*"));
+        Collection<String> matches = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(
+            indicesAndAliasesContext,
+            "*"
+        );
         assertThat(matches, containsInAnyOrder("bar_bar", "foo_foo", "foo_index", "bar_index"));
-        matches = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(onlyIndicesContext, List.of("*"));
+        matches = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(onlyIndicesContext, "*");
         assertThat(matches, containsInAnyOrder("bar_bar", "foo_foo", "foo_index", "bar_index"));
-        matches = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(indicesAndAliasesContext, List.of("foo*"));
+        matches = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(indicesAndAliasesContext, "foo*");
         assertThat(matches, containsInAnyOrder("foo_foo", "foo_index", "bar_index"));
-        matches = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(onlyIndicesContext, List.of("foo*"));
+        matches = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(onlyIndicesContext, "foo*");
         assertThat(matches, containsInAnyOrder("foo_foo", "foo_index"));
-        matches = IndexNameExpressionResolver.WildcardExpressionResolver.resolve(indicesAndAliasesContext, List.of("foo_alias"));
-        assertThat(matches, containsInAnyOrder("foo_alias"));
-        IllegalArgumentException iae = expectThrows(
-            IllegalArgumentException.class,
-            () -> IndexNameExpressionResolver.resolveExpressions(onlyIndicesContext, "foo_alias")
-        );
-        assertThat(
-            iae.getMessage(),
-            containsString("The provided expression [foo_alias] matches an alias, specify the corresponding concrete indices instead")
-        );
     }
 
     private static IndexMetadata.Builder indexBuilder(String index, boolean hidden) {
@@ -838,10 +646,6 @@ public class WildcardExpressionResolverTests extends ESTestCase {
     }
 
     private static void assertWildcardResolvesToEmpty(IndexNameExpressionResolver.Context context, String wildcardExpression) {
-        IndexNotFoundException infe = expectThrows(
-            IndexNotFoundException.class,
-            () -> IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, List.of(wildcardExpression))
-        );
-        assertEquals(wildcardExpression, infe.getIndex().getName());
+        assertThat(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, wildcardExpression), empty());
     }
 }

+ 3 - 3
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStep.java

@@ -130,11 +130,11 @@ public class GenerateSnapshotNameStep extends ClusterStateActionStep {
      * still result in unique snapshot names.
      */
     public static String generateSnapshotName(String name) {
-        return generateSnapshotName(name, new IndexNameExpressionResolver.ResolverContext());
+        return generateSnapshotName(name, System.currentTimeMillis());
     }
 
-    public static String generateSnapshotName(String name, IndexNameExpressionResolver.Context context) {
-        String candidate = IndexNameExpressionResolver.resolveDateMathExpression(name, context.getStartTime());
+    public static String generateSnapshotName(String name, long now) {
+        String candidate = IndexNameExpressionResolver.resolveDateMathExpression(name, now);
         // TODO: we are breaking the rules of UUIDs by lowercasing this here, find an alternative (snapshot names must be lowercase)
         return candidate + "-" + UUIDs.randomBase64UUID().toLowerCase(Locale.ROOT);
     }

+ 4 - 6
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStepTests.java

@@ -9,7 +9,6 @@ package org.elasticsearch.xpack.core.ilm;
 import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
-import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
 import org.elasticsearch.cluster.metadata.LifecycleExecutionState;
 import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.cluster.metadata.RepositoriesMetadata;
@@ -185,13 +184,12 @@ public class GenerateSnapshotNameStepTests extends AbstractStepTestCase<Generate
         assertThat(generateSnapshotName("name"), startsWith("name-"));
         assertThat(generateSnapshotName("name").length(), greaterThan("name-".length()));
 
-        IndexNameExpressionResolver.ResolverContext resolverContext = new IndexNameExpressionResolver.ResolverContext(time);
-        assertThat(generateSnapshotName("<name-{now}>", resolverContext), startsWith("name-2019.03.15-"));
-        assertThat(generateSnapshotName("<name-{now}>", resolverContext).length(), greaterThan("name-2019.03.15-".length()));
+        assertThat(generateSnapshotName("<name-{now}>", time), startsWith("name-2019.03.15-"));
+        assertThat(generateSnapshotName("<name-{now}>", time).length(), greaterThan("name-2019.03.15-".length()));
 
-        assertThat(generateSnapshotName("<name-{now/M}>", resolverContext), startsWith("name-2019.03.01-"));
+        assertThat(generateSnapshotName("<name-{now/M}>", time), startsWith("name-2019.03.01-"));
 
-        assertThat(generateSnapshotName("<name-{now/m{yyyy-MM-dd.HH:mm:ss}}>", resolverContext), startsWith("name-2019-03-15.21:09:00-"));
+        assertThat(generateSnapshotName("<name-{now/m{yyyy-MM-dd.HH:mm:ss}}>", time), startsWith("name-2019-03-15.21:09:00-"));
     }
 
     public void testNameValidation() {