Browse Source

Make the Resolve index API use the canonical expression resolver (#92820)

The original intent of this PR is to make the Resolve Index API
not use the Security's `IndexAbstractionResolver` expression resolver
implementation, and instead use the `IndexNameExpressionResolver` one,
that all the other APIs use.
As a consequence, this PR also fixes a couple of issues with the API's
expression resolver:
 * the `ignore_unavailable` option was hardcoded as `true` (adjusting it to
`false` in the request had no effect) (this option controls if missing explicit
names are ignored or an error is thrown)
 * the `allow_no_indices` option was hardcoded as `true` (adjusting it to
`false` in the request had no effect) (this option controls if wildcards that
expand to empty are allowed or an error is thrown)
 * `_all` index expression was siletly ignored (it now works identical
to the `*` expression)
 * wildcard expansion to system indices was different in subtle ways
Albert Zaharovits 2 years ago
parent
commit
6acf6325ec

+ 53 - 29
server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java

@@ -21,7 +21,6 @@ import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.IndexAbstraction;
-import org.elasticsearch.cluster.metadata.IndexAbstractionResolver;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
 import org.elasticsearch.cluster.metadata.Metadata;
@@ -31,6 +30,7 @@ import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.regex.Regex;
 import org.elasticsearch.common.util.concurrent.CountDown;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.index.Index;
@@ -53,6 +53,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
 
@@ -137,6 +138,7 @@ public class ResolveIndexAction extends ActionType<ResolveIndexAction.Response>
 
         @Override
         public boolean includeDataStreams() {
+            // request must allow data streams because the index name expression resolver for the action handler assumes it
             return true;
         }
     }
@@ -439,7 +441,7 @@ public class ResolveIndexAction extends ActionType<ResolveIndexAction.Response>
         private final ThreadPool threadPool;
         private final ClusterService clusterService;
         private final RemoteClusterService remoteClusterService;
-        private final IndexAbstractionResolver indexAbstractionResolver;
+        private final IndexNameExpressionResolver indexNameExpressionResolver;
         private final boolean ccsCheckCompatibility;
 
         @Inject
@@ -454,7 +456,7 @@ public class ResolveIndexAction extends ActionType<ResolveIndexAction.Response>
             this.threadPool = threadPool;
             this.clusterService = clusterService;
             this.remoteClusterService = transportService.getRemoteClusterService();
-            this.indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver);
+            this.indexNameExpressionResolver = indexNameExpressionResolver;
             this.ccsCheckCompatibility = SearchService.CCS_VERSION_CHECK_SETTING.get(clusterService.getSettings());
         }
 
@@ -469,22 +471,10 @@ public class ResolveIndexAction extends ActionType<ResolveIndexAction.Response>
                 request.indices()
             );
             final OriginalIndices localIndices = remoteClusterIndices.remove(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY);
-            final Metadata metadata = clusterState.metadata();
             List<ResolvedIndex> indices = new ArrayList<>();
             List<ResolvedAlias> aliases = new ArrayList<>();
             List<ResolvedDataStream> dataStreams = new ArrayList<>();
-            if (localIndices != null) {
-                resolveIndices(
-                    localIndices.indices(),
-                    request.indicesOptions,
-                    metadata,
-                    indexAbstractionResolver,
-                    indices,
-                    aliases,
-                    dataStreams,
-                    request.includeDataStreams()
-                );
-            }
+            resolveIndices(localIndices, clusterState, indexNameExpressionResolver, indices, aliases, dataStreams);
 
             if (remoteClusterIndices.size() > 0) {
                 final int remoteRequests = remoteClusterIndices.size();
@@ -513,12 +503,36 @@ public class ResolveIndexAction extends ActionType<ResolveIndexAction.Response>
             }
         }
 
+        /**
+         * Resolves the specified names and/or wildcard expressions to index abstractions. Returns results in the supplied lists.
+         *
+         * @param localIndices   The names and wildcard expressions to resolve
+         * @param clusterState   Cluster state
+         * @param resolver       Resolver instance for matching names
+         * @param indices        List containing any matching indices
+         * @param aliases        List containing any matching aliases
+         * @param dataStreams    List containing any matching data streams
+         */
+        static void resolveIndices(
+            @Nullable OriginalIndices localIndices,
+            ClusterState clusterState,
+            IndexNameExpressionResolver resolver,
+            List<ResolvedIndex> indices,
+            List<ResolvedAlias> aliases,
+            List<ResolvedDataStream> dataStreams
+        ) {
+            if (localIndices == null) {
+                return;
+            }
+            resolveIndices(localIndices.indices(), localIndices.indicesOptions(), clusterState, resolver, indices, aliases, dataStreams);
+        }
+
         /**
          * Resolves the specified names and/or wildcard expressions to index abstractions. Returns results in the supplied lists.
          *
          * @param names          The names and wildcard expressions to resolve
          * @param indicesOptions Options for expanding wildcards to indices with different states
-         * @param metadata       Cluster metadata
+         * @param clusterState   Cluster state
          * @param resolver       Resolver instance for matching names
          * @param indices        List containing any matching indices
          * @param aliases        List containing any matching aliases
@@ -528,22 +542,33 @@ public class ResolveIndexAction extends ActionType<ResolveIndexAction.Response>
         static void resolveIndices(
             String[] names,
             IndicesOptions indicesOptions,
-            Metadata metadata,
-            IndexAbstractionResolver resolver,
+            ClusterState clusterState,
+            IndexNameExpressionResolver resolver,
             List<ResolvedIndex> indices,
             List<ResolvedAlias> aliases,
-            List<ResolvedDataStream> dataStreams,
-            boolean includeDataStreams
+            List<ResolvedDataStream> dataStreams
         ) {
-            List<String> resolvedIndexAbstractions = resolver.resolveIndexAbstractions(names, indicesOptions, metadata, includeDataStreams);
-            SortedMap<String, IndexAbstraction> lookup = metadata.getIndicesLookup();
+            // redundant check to ensure that we don't resolve the list of empty names to "all" in this context
+            if (names.length == 0) {
+                return;
+            }
+            // TODO This is a dirty hack around the IndexNameExpressionResolver optimisation for "*" as described in:
+            // https://github.com/elastic/elasticsearch/issues/92903.
+            // A standalone "*" expression is resolved slightly differently from a "*" embedded in another expression, eg "idx,*".
+            // The difference is only slight, and it usually doesn't cause problems (see
+            // https://github.com/elastic/elasticsearch/issues/92911 for a description of a problem).
+            // But in the case of the Resolve index API, the difference is observable, because resolving standalone "*" cannot show
+            // aliases (only indices and datastreams). The Resolve index API needs to show the aliases that match wildcards.
+            if (names.length == 1 && (Metadata.ALL.equals(names[0]) || Regex.isMatchAllPattern(names[0]))) {
+                names = new String[] { "**" };
+            }
+            Set<String> resolvedIndexAbstractions = resolver.resolveExpressions(clusterState, indicesOptions, true, names);
             for (String s : resolvedIndexAbstractions) {
-                enrichIndexAbstraction(metadata, s, lookup, indices, aliases, dataStreams);
+                enrichIndexAbstraction(clusterState, s, indices, aliases, dataStreams);
             }
             indices.sort(Comparator.comparing(ResolvedIndexAbstraction::getName));
             aliases.sort(Comparator.comparing(ResolvedIndexAbstraction::getName));
             dataStreams.sort(Comparator.comparing(ResolvedIndexAbstraction::getName));
-
         }
 
         private static void mergeResults(
@@ -568,18 +593,17 @@ public class ResolveIndexAction extends ActionType<ResolveIndexAction.Response>
         }
 
         private static void enrichIndexAbstraction(
-            Metadata metadata,
+            ClusterState clusterState,
             String indexAbstraction,
-            SortedMap<String, IndexAbstraction> lookup,
             List<ResolvedIndex> indices,
             List<ResolvedAlias> aliases,
             List<ResolvedDataStream> dataStreams
         ) {
-            IndexAbstraction ia = lookup.get(indexAbstraction);
+            IndexAbstraction ia = clusterState.metadata().getIndicesLookup().get(indexAbstraction);
             if (ia != null) {
                 switch (ia.getType()) {
                     case CONCRETE_INDEX -> {
-                        IndexMetadata writeIndex = metadata.index(ia.getWriteIndex());
+                        IndexMetadata writeIndex = clusterState.metadata().index(ia.getWriteIndex());
                         String[] aliasNames = writeIndex.getAliases().keySet().stream().sorted().toArray(String[]::new);
                         List<Attribute> attributes = new ArrayList<>();
                         attributes.add(writeIndex.getState() == IndexMetadata.State.OPEN ? Attribute.OPEN : Attribute.CLOSED);

+ 0 - 27
server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java

@@ -14,7 +14,6 @@ import org.elasticsearch.index.IndexNotFoundException;
 import org.elasticsearch.indices.SystemIndices.SystemIndexAccessLevel;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -29,32 +28,6 @@ public class IndexAbstractionResolver {
         this.indexNameExpressionResolver = indexNameExpressionResolver;
     }
 
-    public List<String> resolveIndexAbstractions(
-        String[] indices,
-        IndicesOptions indicesOptions,
-        Metadata metadata,
-        boolean includeDataStreams
-    ) {
-        return resolveIndexAbstractions(Arrays.asList(indices), indicesOptions, metadata, includeDataStreams);
-    }
-
-    public List<String> resolveIndexAbstractions(
-        Iterable<String> indices,
-        IndicesOptions indicesOptions,
-        Metadata metadata,
-        boolean includeDataStreams
-    ) {
-        final Set<String> availableIndexAbstractions = metadata.getIndicesLookup().keySet();
-        return resolveIndexAbstractions(
-            indices,
-            indicesOptions,
-            metadata,
-            () -> availableIndexAbstractions,
-            availableIndexAbstractions::contains,
-            includeDataStreams
-        );
-    }
-
     public List<String> resolveIndexAbstractions(
         Iterable<String> indices,
         IndicesOptions indicesOptions,

+ 16 - 1
server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java

@@ -615,12 +615,27 @@ public class IndexNameExpressionResolver {
      * Resolve an array of expressions to the set of indices and aliases that these expressions match.
      */
     public Set<String> resolveExpressions(ClusterState state, String... expressions) {
+        return resolveExpressions(state, IndicesOptions.lenientExpandOpen(), false, expressions);
+    }
+
+    /**
+     * Resolve the expression to the set of indices, aliases, and, optionally, datastreams that the expression matches.
+     * If {@param preserveDataStreams} is {@code true}, datastreams that are covered by the wildcards from the
+     * {@param expressions} are returned as-is, without expanding them further to their respective backing indices.
+     */
+    public Set<String> resolveExpressions(
+        ClusterState state,
+        IndicesOptions indicesOptions,
+        boolean preserveDataStreams,
+        String... expressions
+    ) {
         Context context = new Context(
             state,
-            IndicesOptions.lenientExpandOpen(),
+            indicesOptions,
             true,
             false,
             true,
+            preserveDataStreams,
             getSystemIndexAccessLevel(),
             getSystemIndexAccessPredicate(),
             getNetNewSystemIndexPredicate()

+ 176 - 35
server/src/test/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexTests.java

@@ -15,15 +15,22 @@ import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction.Resolve
 import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction.ResolvedIndex;
 import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction.TransportAction;
 import org.elasticsearch.action.support.IndicesOptions;
+import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.AliasMetadata;
 import org.elasticsearch.cluster.metadata.DataStream;
 import org.elasticsearch.cluster.metadata.DataStreamTestHelper;
-import org.elasticsearch.cluster.metadata.IndexAbstractionResolver;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
 import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.time.DateFormatter;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.index.IndexNotFoundException;
+import org.elasticsearch.indices.EmptySystemIndices;
+import org.elasticsearch.indices.SystemIndexDescriptor;
+import org.elasticsearch.indices.SystemIndices;
 import org.elasticsearch.indices.TestIndexNameExpressionResolver;
 import org.elasticsearch.test.ESTestCase;
 import org.junit.Before;
@@ -38,10 +45,17 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
+import static org.elasticsearch.indices.SystemIndices.EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY;
+import static org.elasticsearch.indices.SystemIndices.SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY;
 import static org.hamcrest.Matchers.arrayContaining;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.oneOf;
 import static org.hamcrest.core.IsNull.notNullValue;
 
@@ -62,8 +76,10 @@ public class ResolveIndexTests extends ESTestCase {
         { "logs-mysql-prod", 4 },
         { "logs-mysql-test", 2 } };
 
-    private Metadata metadata;
-    private final IndexAbstractionResolver resolver = new IndexAbstractionResolver(TestIndexNameExpressionResolver.newInstance());
+    private ClusterState clusterState;
+    private ThreadContext threadContext;
+
+    private IndexNameExpressionResolver resolver = TestIndexNameExpressionResolver.newInstance();
 
     private long epochMillis;
     private String dateString;
@@ -71,8 +87,10 @@ public class ResolveIndexTests extends ESTestCase {
     @Before
     public void setup() {
         epochMillis = randomLongBetween(1580536800000L, 1583042400000L);
+        threadContext = createThreadContext();
+        resolver = new IndexNameExpressionResolver(threadContext, EmptySystemIndices.INSTANCE);
         dateString = DataStream.DATE_FORMATTER.formatMillis(epochMillis);
-        metadata = buildMetadata(dataStreams, indices);
+        clusterState = ClusterState.builder(new ClusterName("_name")).metadata(buildMetadata(dataStreams, indices)).build();
     }
 
     public void testResolveStarWithDefaultOptions() {
@@ -82,7 +100,7 @@ public class ResolveIndexTests extends ESTestCase {
         List<ResolvedAlias> aliases = new ArrayList<>();
         List<ResolvedDataStream> dataStreams = new ArrayList<>();
 
-        TransportAction.resolveIndices(names, indicesOptions, metadata, resolver, indices, aliases, dataStreams, true);
+        TransportAction.resolveIndices(names, indicesOptions, clusterState, resolver, indices, aliases, dataStreams);
 
         validateIndices(
             indices,
@@ -100,13 +118,16 @@ public class ResolveIndexTests extends ESTestCase {
     }
 
     public void testResolveStarWithAllOptions() {
-        String[] names = new String[] { "*" };
-        IndicesOptions indicesOptions = IndicesOptions.LENIENT_EXPAND_OPEN_CLOSED_HIDDEN;
+        String[] names = randomFrom(new String[] { "*" }, new String[] { "_all" });
+        IndicesOptions indicesOptions = randomFrom(
+            IndicesOptions.LENIENT_EXPAND_OPEN_CLOSED_HIDDEN,
+            IndicesOptions.STRICT_EXPAND_OPEN_CLOSED_HIDDEN
+        );
         List<ResolvedIndex> indices = new ArrayList<>();
         List<ResolvedAlias> aliases = new ArrayList<>();
         List<ResolvedDataStream> dataStreams = new ArrayList<>();
 
-        TransportAction.resolveIndices(names, indicesOptions, metadata, resolver, indices, aliases, dataStreams, true);
+        TransportAction.resolveIndices(names, indicesOptions, clusterState, resolver, indices, aliases, dataStreams);
         validateIndices(
             indices,
             ".ds-logs-mysql-prod-" + dateString + "-000001",
@@ -136,7 +157,7 @@ public class ResolveIndexTests extends ESTestCase {
         List<ResolvedAlias> aliases = new ArrayList<>();
         List<ResolvedDataStream> dataStreams = new ArrayList<>();
 
-        TransportAction.resolveIndices(names, indicesOptions, metadata, resolver, indices, aliases, dataStreams, true);
+        TransportAction.resolveIndices(names, indicesOptions, clusterState, resolver, indices, aliases, dataStreams);
 
         validateIndices(
             indices,
@@ -163,7 +184,7 @@ public class ResolveIndexTests extends ESTestCase {
         List<ResolvedAlias> aliases = new ArrayList<>();
         List<ResolvedDataStream> dataStreams = new ArrayList<>();
 
-        TransportAction.resolveIndices(names, indicesOptions, metadata, resolver, indices, aliases, dataStreams, true);
+        TransportAction.resolveIndices(names, indicesOptions, clusterState, resolver, indices, aliases, dataStreams);
         validateIndices(indices, ".ds-logs-mysql-prod-" + dateString + "-000003", "logs-pgsql-test-20200102");
         validateAliases(aliases, "one-off-alias");
         validateDataStreams(dataStreams, "logs-mysql-test");
@@ -180,21 +201,13 @@ public class ResolveIndexTests extends ESTestCase {
 
         DataStream ds = DataStreamTestHelper.newInstance(dataStreamName, backingIndices.stream().map(IndexMetadata::getIndex).toList());
         builder.put(ds);
+        ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).metadata(builder.build()).build();
 
         IndicesOptions indicesOptions = IndicesOptions.LENIENT_EXPAND_OPEN_CLOSED_HIDDEN;
         List<ResolvedIndex> indices = new ArrayList<>();
         List<ResolvedAlias> aliases = new ArrayList<>();
         List<ResolvedDataStream> dataStreams = new ArrayList<>();
-        TransportAction.resolveIndices(
-            new String[] { "*" },
-            indicesOptions,
-            builder.build(),
-            resolver,
-            indices,
-            aliases,
-            dataStreams,
-            true
-        );
+        TransportAction.resolveIndices(new String[] { "*" }, indicesOptions, clusterState, resolver, indices, aliases, dataStreams);
 
         assertThat(dataStreams.size(), equalTo(1));
         assertThat(dataStreams.get(0).getBackingIndices(), arrayContaining(names));
@@ -210,20 +223,98 @@ public class ResolveIndexTests extends ESTestCase {
             // name, isClosed, isHidden, isFrozen, dataStream, aliases
             { "logs-pgsql-prod-" + todaySuffix, false, true, false, false, null, Strings.EMPTY_ARRAY },
             { "logs-pgsql-prod-" + tomorrowSuffix, false, true, false, false, null, Strings.EMPTY_ARRAY } };
-        Metadata metadata = buildMetadata(new Object[][] {}, indices);
-        Set<String> authorizedIndices = Set.of("logs-pgsql-prod-" + todaySuffix, "logs-pgsql-prod-" + tomorrowSuffix);
-
-        String requestedIndex = "<logs-pgsql-prod-{now/d}>";
-        List<String> resolvedIndices = resolver.resolveIndexAbstractions(
-            List.of(requestedIndex),
-            IndicesOptions.LENIENT_EXPAND_OPEN,
-            metadata,
-            () -> authorizedIndices,
-            authorizedIndices::contains,
-            randomBoolean()
-        );
+        ClusterState clusterState = ClusterState.builder(new ClusterName("_name"))
+            .metadata(buildMetadata(new Object[][] {}, indices))
+            .build();
+        String[] requestedIndex = new String[] { "<logs-pgsql-prod-{now/d}>" };
+        Set<String> resolvedIndices = resolver.resolveExpressions(clusterState, IndicesOptions.LENIENT_EXPAND_OPEN, true, requestedIndex);
         assertThat(resolvedIndices.size(), is(1));
-        assertThat(resolvedIndices.get(0), oneOf("logs-pgsql-prod-" + todaySuffix, "logs-pgsql-prod-" + tomorrowSuffix));
+        assertThat(resolvedIndices, contains(oneOf("logs-pgsql-prod-" + todaySuffix, "logs-pgsql-prod-" + tomorrowSuffix)));
+    }
+
+    public void testSystemIndexAccess() {
+        Metadata.Builder mdBuilder = buildMetadata(dataStreams, indices);
+        SystemIndices systemIndices = addSystemIndex(mdBuilder);
+        clusterState = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build();
+        {
+            threadContext = createThreadContext();
+            if (randomBoolean()) {
+                threadContext.putHeader(SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, "false");
+            }
+            threadContext.putHeader(EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, "whatever");
+            resolver = new IndexNameExpressionResolver(threadContext, systemIndices);
+            List<ResolvedIndex> indices = new ArrayList<>();
+            List<ResolvedAlias> aliases = new ArrayList<>();
+            List<ResolvedDataStream> dataStreams = new ArrayList<>();
+            TransportAction.resolveIndices(
+                new String[] { ".*" },
+                IndicesOptions.LENIENT_EXPAND_OPEN,
+                clusterState,
+                resolver,
+                indices,
+                aliases,
+                dataStreams
+            );
+            // non net-new system indices are allowed even when no system indices are allowed
+            assertThat(indices.stream().map(ResolvedIndex::getName).collect(Collectors.toList()), hasItem(is(".test-system-1")));
+        }
+        {
+            threadContext = createThreadContext();
+            threadContext.putHeader(EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, "test-net-new-system");
+            resolver = new IndexNameExpressionResolver(threadContext, systemIndices);
+            List<ResolvedIndex> indices = new ArrayList<>();
+            List<ResolvedAlias> aliases = new ArrayList<>();
+            List<ResolvedDataStream> dataStreams = new ArrayList<>();
+            TransportAction.resolveIndices(
+                new String[] { ".*" },
+                IndicesOptions.STRICT_EXPAND_OPEN,
+                clusterState,
+                resolver,
+                indices,
+                aliases,
+                dataStreams
+            );
+            assertThat(indices.stream().map(ResolvedIndex::getName).collect(Collectors.toList()), hasItem(is(".test-system-1")));
+            assertThat(indices.stream().map(ResolvedIndex::getName).collect(Collectors.toList()), hasItem(is(".test-net-new-system-1")));
+            indices = new ArrayList<>();
+            TransportAction.resolveIndices(
+                new String[] { ".test-net*" },
+                IndicesOptions.STRICT_EXPAND_OPEN,
+                clusterState,
+                resolver,
+                indices,
+                aliases,
+                dataStreams
+            );
+            assertThat(indices.stream().map(ResolvedIndex::getName).collect(Collectors.toList()), not(hasItem(is(".test-system-1"))));
+            assertThat(indices.stream().map(ResolvedIndex::getName).collect(Collectors.toList()), hasItem(is(".test-net-new-system-1")));
+        }
+    }
+
+    public void testIgnoreUnavailableFalse() {
+        String[] names = new String[] { "missing", "logs*" };
+        IndicesOptions indicesOptions = IndicesOptions.STRICT_EXPAND_OPEN;
+        List<ResolvedIndex> indices = new ArrayList<>();
+        List<ResolvedAlias> aliases = new ArrayList<>();
+        List<ResolvedDataStream> dataStreams = new ArrayList<>();
+        IndexNotFoundException infe = expectThrows(
+            IndexNotFoundException.class,
+            () -> TransportAction.resolveIndices(names, indicesOptions, clusterState, resolver, indices, aliases, dataStreams)
+        );
+        assertThat(infe.getMessage(), containsString("no such index [missing]"));
+    }
+
+    public void testAllowNoIndicesFalse() {
+        String[] names = new String[] { "missing", "missing*" };
+        IndicesOptions indicesOptions = IndicesOptions.fromOptions(true, false, true, true);
+        List<ResolvedIndex> indices = new ArrayList<>();
+        List<ResolvedAlias> aliases = new ArrayList<>();
+        List<ResolvedDataStream> dataStreams = new ArrayList<>();
+        IndexNotFoundException infe = expectThrows(
+            IndexNotFoundException.class,
+            () -> TransportAction.resolveIndices(names, indicesOptions, clusterState, resolver, indices, aliases, dataStreams)
+        );
+        assertThat(infe.getMessage(), containsString("no such index [missing*]"));
     }
 
     private void validateIndices(List<ResolvedIndex> resolvedIndices, String... expectedIndices) {
@@ -285,7 +376,7 @@ public class ResolveIndexTests extends ESTestCase {
         }
     }
 
-    Metadata buildMetadata(Object[][] dataStreams, Object[][] indices) {
+    Metadata.Builder buildMetadata(Object[][] dataStreams, Object[][] indices) {
         Metadata.Builder builder = Metadata.builder();
 
         List<IndexMetadata> allIndices = new ArrayList<>();
@@ -318,7 +409,7 @@ public class ResolveIndexTests extends ESTestCase {
             builder.put(index, false);
         }
 
-        return builder.build();
+        return builder;
     }
 
     private static IndexMetadata createIndexMetadata(
@@ -396,4 +487,54 @@ public class ResolveIndexTests extends ESTestCase {
         attributes.sort(String::compareTo);
         return attributes.toArray(Strings.EMPTY_ARRAY);
     }
+
+    private SystemIndices addSystemIndex(Metadata.Builder mdBuilder) {
+        mdBuilder.put(indexBuilder(".test-system-1", SystemIndexDescriptor.DEFAULT_SETTINGS).state(IndexMetadata.State.OPEN).system(true))
+            .put(
+                indexBuilder(".test-net-new-system-1", SystemIndexDescriptor.DEFAULT_SETTINGS).state(IndexMetadata.State.OPEN).system(true)
+            );
+        SystemIndices systemIndices = new SystemIndices(
+            List.of(
+                new SystemIndices.Feature(
+                    "test-system-feature",
+                    "test system index",
+                    List.of(
+                        new SystemIndexDescriptor(
+                            ".test-system*",
+                            "test-system-description",
+                            SystemIndexDescriptor.Type.EXTERNAL_UNMANAGED,
+                            List.of("test-system")
+                        ),
+                        SystemIndexDescriptor.builder()
+                            .setIndexPattern(".test-net-new-system*")
+                            .setDescription("test-net-new-system-description")
+                            .setType(SystemIndexDescriptor.Type.EXTERNAL_MANAGED)
+                            .setAllowedElasticProductOrigins(List.of("test-net-new-system"))
+                            .setNetNew()
+                            .setSettings(Settings.EMPTY)
+                            .setMappings("{ \"_doc\": { \"_meta\": { \"version\": \"8.0.0\" } } }")
+                            .setPrimaryIndex(".test-net-new-system-1")
+                            .setVersionMetaKey("version")
+                            .setOrigin("system")
+                            .build()
+                    )
+                )
+            )
+        );
+        return systemIndices;
+    }
+
+    private static IndexMetadata.Builder indexBuilder(String index, Settings additionalSettings) {
+        return IndexMetadata.builder(index).settings(settings(additionalSettings));
+    }
+
+    private static Settings.Builder settings(Settings additionalSettings) {
+        return settings(Version.CURRENT).put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1)
+            .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)
+            .put(additionalSettings);
+    }
+
+    private ThreadContext createThreadContext() {
+        return new ThreadContext(Settings.EMPTY);
+    }
 }