Jelajahi Sumber

[HealthAPI] Add support for the FEATURE_STATE affected resource (#92296)

The `shards_availability` indicator diagnoses the condition where
indices need to be restored from snapshot.
Starting with 8.0 using feature_states when restoring from snapshot is
mandatory.

This adds support for the `FEATURE_STATE` affected resource to aid with
building up the snapshot restore API call (which will need to include
all the indices and feature states reported by the restore-from-snapshot
diagnosis).

Note that the health API will not report any indices that are part of a
feature state.
Andrei Dan 2 tahun lalu
induk
melakukan
9170113036

+ 3 - 1
benchmarks/src/main/java/org/elasticsearch/benchmark/routing/allocation/ShardsAvailabilityHealthIndicatorBenchmark.java

@@ -29,6 +29,7 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.health.HealthIndicatorResult;
 import org.elasticsearch.health.node.HealthInfo;
 import org.elasticsearch.index.shard.ShardId;
+import org.elasticsearch.indices.SystemIndices;
 import org.elasticsearch.tasks.TaskManager;
 import org.elasticsearch.threadpool.ThreadPool;
 import org.openjdk.jmh.annotations.Benchmark;
@@ -45,6 +46,7 @@ import org.openjdk.jmh.annotations.Warmup;
 
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -171,7 +173,7 @@ public class ShardsAvailabilityHealthIndicatorBenchmark {
             new TaskManager(Settings.EMPTY, threadPool, Collections.emptySet())
         );
         clusterService.getClusterApplierService().setInitialState(initialClusterState);
-        indicatorService = new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService);
+        indicatorService = new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService, new SystemIndices(List.of()));
     }
 
     private int toInt(String v) {

+ 6 - 0
docs/changelog/92296.yaml

@@ -0,0 +1,6 @@
+pr: 92296
+summary: "[HealthAPI] Add support for the FEATURE_STATE affected resource"
+area: Health
+type: feature
+issues:
+ - 91353

+ 30 - 0
docs/reference/tab-widgets/troubleshooting/data/restore-from-snapshot.asciidoc

@@ -210,6 +210,21 @@ POST _snapshot/my_repository/snapshot-20200617/_restore
 <1> The indices to restore.
 +
 <2> We also want to restore the aliases.
++
+NOTE: If any <<feature-state,feature states>> need to be restored we'll need to specify them using the
+`feature_states` field and the indices that belong to the feature states we restore must not be specified under `indices`.
+The <<health-api, Health API>> returns both the `indices` and `feature_states` that need to be restored for the restore from snapshot diagnosis. e.g.:
++
+[source,console]
+----
+POST _snapshot/my_repository/snapshot-20200617/_restore
+{
+  "feature_states": [ "geoip" ],
+  "indices": "kibana_sample_data_flights,.ds-my-data-stream-2022.06.17-000001",
+  "include_aliases": true
+}
+----
+// TEST[skip:illustration purposes only]
 
 . Finally we can verify that the indices health is now `green` via the <<cat-indices,cat indices API>>.
 +
@@ -430,6 +445,21 @@ POST _snapshot/my_repository/snapshot-20200617/_restore
 <1> The indices to restore.
 +
 <2> We also want to restore the aliases.
++
+NOTE: If any <<feature-state,feature states>> need to be restored we'll need to specify them using the
+`feature_states` field and the indices that belong to the feature states we restore must not be specified under `indices`.
+The <<health-api, Health API>> returns both the `indices` and `feature_states` that need to be restored for the restore from snapshot diagnosis. e.g.:
++
+[source,console]
+----
+POST _snapshot/my_repository/snapshot-20200617/_restore
+{
+  "feature_states": [ "geoip" ],
+  "indices": "kibana_sample_data_flights,.ds-my-data-stream-2022.06.17-000001",
+  "include_aliases": true
+}
+----
+// TEST[skip:illustration purposes only]
 
 . Finally we can verify that the indices health is now `green` via the <<cat-indices,cat indices API>>.
 +

+ 110 - 19
server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java

@@ -43,6 +43,7 @@ import org.elasticsearch.health.HealthStatus;
 import org.elasticsearch.health.ImpactArea;
 import org.elasticsearch.health.SimpleHealthIndicatorDetails;
 import org.elasticsearch.health.node.HealthInfo;
+import org.elasticsearch.indices.SystemIndices;
 import org.elasticsearch.snapshots.SnapshotShardSizeInfo;
 
 import java.util.ArrayList;
@@ -59,6 +60,8 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toSet;
 import static org.elasticsearch.cluster.health.ClusterShardHealth.getInactivePrimaryHealth;
 import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_PREFIX;
 import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_SETTING;
@@ -67,6 +70,7 @@ import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQ
 import static org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider.INDEX_ROUTING_ALLOCATION_ENABLE_SETTING;
 import static org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider.CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING;
 import static org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider.INDEX_TOTAL_SHARDS_PER_NODE_SETTING;
+import static org.elasticsearch.health.Diagnosis.Resource.Type.FEATURE_STATE;
 import static org.elasticsearch.health.Diagnosis.Resource.Type.INDEX;
 import static org.elasticsearch.health.HealthStatus.GREEN;
 import static org.elasticsearch.health.HealthStatus.RED;
@@ -96,9 +100,16 @@ public class ShardsAvailabilityHealthIndicatorService implements HealthIndicator
     private final ClusterService clusterService;
     private final AllocationService allocationService;
 
-    public ShardsAvailabilityHealthIndicatorService(ClusterService clusterService, AllocationService allocationService) {
+    private final SystemIndices systemIndices;
+
+    public ShardsAvailabilityHealthIndicatorService(
+        ClusterService clusterService,
+        AllocationService allocationService,
+        SystemIndices systemIndices
+    ) {
         this.clusterService = clusterService;
         this.allocationService = allocationService;
+        this.systemIndices = systemIndices;
     }
 
     @Override
@@ -760,7 +771,7 @@ public class ShardsAvailabilityHealthIndicatorService implements HealthIndicator
         }
     }
 
-    private class ShardAllocationStatus {
+    class ShardAllocationStatus {
         private final ShardAllocationCounts primaries = new ShardAllocationCounts();
         private final ShardAllocationCounts replicas = new ShardAllocationCounts();
         private final Metadata clusterMetadata;
@@ -908,28 +919,108 @@ public class ShardsAvailabilityHealthIndicatorService implements HealthIndicator
                 if (diagnosisToAffectedIndices.isEmpty()) {
                     return List.of();
                 } else {
-                    return diagnosisToAffectedIndices.entrySet()
-                        .stream()
-                        .map(
-                            e -> new Diagnosis(
-                                e.getKey(),
-                                List.of(
-                                    new Diagnosis.Resource(
-                                        INDEX,
-                                        e.getValue()
-                                            .stream()
-                                            .sorted(indicesComparatorByPriorityAndName(clusterMetadata))
-                                            .limit(Math.min(e.getValue().size(), maxAffectedResourcesCount))
-                                            .collect(Collectors.toList())
-                                    )
+
+                    return diagnosisToAffectedIndices.entrySet().stream().map(e -> {
+                        List<Diagnosis.Resource> affectedResources = new ArrayList<>(1);
+                        if (e.getKey().equals(ACTION_RESTORE_FROM_SNAPSHOT)) {
+                            Set<String> restoreFromSnapshotIndices = e.getValue();
+                            if (restoreFromSnapshotIndices != null && restoreFromSnapshotIndices.isEmpty() == false) {
+                                affectedResources = getRestoreFromSnapshotAffectedResources(
+                                    clusterMetadata,
+                                    systemIndices,
+                                    restoreFromSnapshotIndices,
+                                    maxAffectedResourcesCount
+                                );
+                            }
+                        } else {
+                            affectedResources.add(
+                                new Diagnosis.Resource(
+                                    INDEX,
+                                    e.getValue()
+                                        .stream()
+                                        .sorted(indicesComparatorByPriorityAndName(clusterMetadata))
+                                        .limit(Math.min(e.getValue().size(), maxAffectedResourcesCount))
+                                        .collect(Collectors.toList())
                                 )
-                            )
-                        )
-                        .collect(Collectors.toList());
+                            );
+                        }
+                        return new Diagnosis(e.getKey(), affectedResources);
+                    }).collect(Collectors.toList());
                 }
             } else {
                 return List.of();
             }
         }
+
+        /**
+         * The restore from snapshot operation requires the user to specify indices and feature states.
+         * The indices that are part of the feature states must not be specified. This method loops through all the
+         * identified unassigned indices and returns the affected {@link Diagnosis.Resource}s of type `INDEX`
+         * and if applicable `FEATURE_STATE`
+         */
+        static List<Diagnosis.Resource> getRestoreFromSnapshotAffectedResources(
+            Metadata metadata,
+            SystemIndices systemIndices,
+            Set<String> restoreFromSnapshotIndices,
+            int maxAffectedResourcesCount
+        ) {
+            List<Diagnosis.Resource> affectedResources = new ArrayList<>(2);
+
+            Set<String> affectedIndices = new HashSet<>(restoreFromSnapshotIndices);
+            Set<String> affectedFeatureStates = new HashSet<>();
+            Map<String, Set<String>> featureToSystemIndices = systemIndices.getFeatures()
+                .stream()
+                .collect(
+                    toMap(
+                        SystemIndices.Feature::getName,
+                        feature -> feature.getIndexDescriptors()
+                            .stream()
+                            .flatMap(descriptor -> descriptor.getMatchingIndices(metadata).stream())
+                            .collect(toSet())
+                    )
+                );
+
+            for (Map.Entry<String, Set<String>> featureToIndices : featureToSystemIndices.entrySet()) {
+                for (String featureIndex : featureToIndices.getValue()) {
+                    if (restoreFromSnapshotIndices.contains(featureIndex)) {
+                        affectedFeatureStates.add(featureToIndices.getKey());
+                        affectedIndices.remove(featureIndex);
+                    }
+                }
+            }
+
+            Map<String, Set<String>> featureToDsBackingIndices = systemIndices.getFeatures()
+                .stream()
+                .collect(
+                    toMap(
+                        SystemIndices.Feature::getName,
+                        feature -> feature.getDataStreamDescriptors()
+                            .stream()
+                            .flatMap(descriptor -> descriptor.getBackingIndexNames(metadata).stream())
+                            .collect(toSet())
+                    )
+                );
+
+            // the shards_availability indicator works with indices so let's remove the feature states data streams backing indices from
+            // the list of affected indices (the feature state will cover the restore of these indices too)
+            for (Map.Entry<String, Set<String>> featureToBackingIndices : featureToDsBackingIndices.entrySet()) {
+                for (String featureIndex : featureToBackingIndices.getValue()) {
+                    if (restoreFromSnapshotIndices.contains(featureIndex)) {
+                        affectedFeatureStates.add(featureToBackingIndices.getKey());
+                        affectedIndices.remove(featureIndex);
+                    }
+                }
+            }
+
+            if (affectedIndices.isEmpty() == false) {
+                affectedResources.add(new Diagnosis.Resource(INDEX, affectedIndices.stream().limit(maxAffectedResourcesCount).toList()));
+            }
+            if (affectedFeatureStates.isEmpty() == false) {
+                affectedResources.add(
+                    new Diagnosis.Resource(FEATURE_STATE, affectedFeatureStates.stream().limit(maxAffectedResourcesCount).toList())
+                );
+            }
+            return affectedResources;
+        }
     }
 }

+ 1 - 0
server/src/main/java/org/elasticsearch/health/Diagnosis.java

@@ -44,6 +44,7 @@ public record Diagnosis(Definition definition, @Nullable List<Resource> affected
             INDEX("indices"),
             NODE("nodes"),
             SLM_POLICY("slm_policies"),
+            FEATURE_STATE("feature_states"),
             SNAPSHOT_REPOSITORY("snapshot_repositories");
 
             private final String displayValue;

+ 10 - 3
server/src/main/java/org/elasticsearch/node/Node.java

@@ -997,7 +997,13 @@ public class Node implements Closeable {
                 discoveryModule.getCoordinator(),
                 masterHistoryService
             );
-            HealthService healthService = createHealthService(clusterService, clusterModule, coordinationDiagnosticsService, threadPool);
+            HealthService healthService = createHealthService(
+                clusterService,
+                clusterModule,
+                coordinationDiagnosticsService,
+                threadPool,
+                systemIndices
+            );
             HealthMetadataService healthMetadataService = HealthMetadataService.create(clusterService, settings);
             LocalHealthMonitor localHealthMonitor = LocalHealthMonitor.create(settings, clusterService, nodeService, threadPool, client);
             HealthInfoCache nodeHealthOverview = HealthInfoCache.create(clusterService);
@@ -1199,7 +1205,8 @@ public class Node implements Closeable {
         ClusterService clusterService,
         ClusterModule clusterModule,
         CoordinationDiagnosticsService coordinationDiagnosticsService,
-        ThreadPool threadPool
+        ThreadPool threadPool,
+        SystemIndices systemIndices
     ) {
         List<HealthIndicatorService> preflightHealthIndicatorServices = Collections.singletonList(
             new StableMasterHealthIndicatorService(coordinationDiagnosticsService, clusterService)
@@ -1207,7 +1214,7 @@ public class Node implements Closeable {
         var serverHealthIndicatorServices = new ArrayList<>(
             List.of(
                 new RepositoryIntegrityHealthIndicatorService(clusterService),
-                new ShardsAvailabilityHealthIndicatorService(clusterService, clusterModule.getAllocationService())
+                new ShardsAvailabilityHealthIndicatorService(clusterService, clusterModule.getAllocationService(), systemIndices)
             )
         );
         serverHealthIndicatorServices.add(new DiskHealthIndicatorService(clusterService));

+ 227 - 14
server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java

@@ -11,6 +11,7 @@ package org.elasticsearch.cluster.routing.allocation;
 import org.elasticsearch.Version;
 import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.ComposableIndexTemplate;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.cluster.metadata.NodesShutdownMetadata;
@@ -23,6 +24,7 @@ import org.elasticsearch.cluster.routing.RecoverySource;
 import org.elasticsearch.cluster.routing.RoutingTable;
 import org.elasticsearch.cluster.routing.ShardRouting;
 import org.elasticsearch.cluster.routing.UnassignedInfo;
+import org.elasticsearch.cluster.routing.allocation.ShardsAvailabilityHealthIndicatorService.ShardAllocationStatus;
 import org.elasticsearch.cluster.routing.allocation.decider.AwarenessAllocationDecider;
 import org.elasticsearch.cluster.routing.allocation.decider.Decision;
 import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider;
@@ -43,7 +45,12 @@ import org.elasticsearch.health.SimpleHealthIndicatorDetails;
 import org.elasticsearch.health.node.HealthInfo;
 import org.elasticsearch.index.Index;
 import org.elasticsearch.index.shard.ShardId;
+import org.elasticsearch.indices.ExecutorNames;
+import org.elasticsearch.indices.SystemDataStreamDescriptor;
+import org.elasticsearch.indices.SystemIndexDescriptor;
+import org.elasticsearch.indices.SystemIndices;
 import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.threadpool.ThreadPool;
 import org.mockito.stubbing.Answer;
 
 import java.util.ArrayList;
@@ -56,7 +63,10 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
+import static java.util.Collections.emptyList;
 import static java.util.stream.Collectors.toMap;
+import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.createBackingIndex;
+import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.newInstance;
 import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_PREFIX;
 import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_PREFIX;
 import static org.elasticsearch.cluster.metadata.SingleNodeShutdownMetadata.Type.RESTART;
@@ -83,14 +93,18 @@ import static org.elasticsearch.cluster.routing.allocation.ShardsAvailabilityHea
 import static org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider.CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING;
 import static org.elasticsearch.common.util.CollectionUtils.concatLists;
 import static org.elasticsearch.core.TimeValue.timeValueSeconds;
+import static org.elasticsearch.health.Diagnosis.Resource.Type.FEATURE_STATE;
 import static org.elasticsearch.health.Diagnosis.Resource.Type.INDEX;
 import static org.elasticsearch.health.HealthStatus.GREEN;
 import static org.elasticsearch.health.HealthStatus.RED;
 import static org.elasticsearch.health.HealthStatus.YELLOW;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.emptyCollectionOf;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasItems;
 import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -114,8 +128,8 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
                     GREEN,
                     "This cluster has all shards available.",
                     Map.of("started_primaries", 2, "started_replicas", 1),
-                    Collections.emptyList(),
-                    Collections.emptyList()
+                    emptyList(),
+                    emptyList()
                 )
             )
         );
@@ -353,8 +367,8 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
                     GREEN,
                     "This cluster has 1 restarting replica shard.",
                     Map.of("started_primaries", 1, "restarting_replicas", 1),
-                    Collections.emptyList(),
-                    Collections.emptyList()
+                    emptyList(),
+                    emptyList()
                 )
             )
         );
@@ -374,8 +388,8 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
                     GREEN,
                     "This cluster has all shards available.",
                     Map.of("started_primaries", 1),
-                    Collections.emptyList(),
-                    Collections.emptyList()
+                    emptyList(),
+                    emptyList()
                 )
             )
         );
@@ -436,8 +450,8 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
                     GREEN,
                     "This cluster has 1 creating primary shard.",
                     Map.of("creating_primaries", 1),
-                    Collections.emptyList(),
-                    Collections.emptyList()
+                    emptyList(),
+                    emptyList()
                 )
             )
         );
@@ -457,8 +471,8 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
                     GREEN,
                     "This cluster has 1 restarting primary shard.",
                     Map.of("restarting_primaries", 1),
-                    Collections.emptyList(),
-                    Collections.emptyList()
+                    emptyList(),
+                    emptyList()
                 )
             )
         );
@@ -503,7 +517,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
         );
     }
 
-    public void testUserActionsNotGeneratedWhenNotDrillingDown() {
+    public void testDiagnosisNotGeneratedWhenNotDrillingDown() {
         // Index definition, 1 primary no replicas
         IndexMetadata indexMetadata = IndexMetadata.builder("red-index")
             .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build())
@@ -562,6 +576,149 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
         assertThat(definitions, contains(ACTION_RESTORE_FROM_SNAPSHOT));
     }
 
+    public void testRestoreFromSnapshotReportsFeatureStates() {
+        // this test adds a mix of regular and system indices and data streams
+        // we'll test the `shards_availability` indicator correctly reports the
+        // affected feature states and indices
+
+        IndexMetadata featureIndex = IndexMetadata.builder(".feature-index")
+            .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build())
+            .numberOfShards(1)
+            .numberOfReplicas(0)
+            .build();
+
+        IndexMetadata regularIndex = IndexMetadata.builder("regular-index")
+            .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build())
+            .numberOfShards(1)
+            .numberOfReplicas(0)
+            .build();
+
+        String featureDataStreamName = ".test-ds-feature";
+        IndexMetadata backingIndex = createBackingIndex(featureDataStreamName, 1).build();
+
+        ShardRouting featureIndexRouting = createShardRouting(
+            new ShardId(featureIndex.getIndex(), 0),
+            true,
+            new ShardAllocation(randomNodeId(), UNAVAILABLE, noShardCopy())
+        );
+
+        ShardRouting regularIndexRouting = createShardRouting(
+            new ShardId(regularIndex.getIndex(), 0),
+            true,
+            new ShardAllocation(randomNodeId(), UNAVAILABLE, noShardCopy())
+        );
+
+        ShardRouting backingIndexRouting = createShardRouting(
+            new ShardId(backingIndex.getIndex(), 0),
+            true,
+            new ShardAllocation(randomNodeId(), UNAVAILABLE, noShardCopy())
+        );
+
+        var clusterState = createClusterStateWith(
+            List.of(featureIndex, regularIndex, backingIndex),
+            List.of(
+                IndexRoutingTable.builder(featureIndex.getIndex()).addShard(featureIndexRouting).build(),
+                IndexRoutingTable.builder(regularIndex.getIndex()).addShard(regularIndexRouting).build(),
+                IndexRoutingTable.builder(backingIndex.getIndex()).addShard(backingIndexRouting).build()
+            ),
+            List.of(),
+            List.of()
+        );
+
+        // add the data stream to the cluster state
+        Metadata.Builder mdBuilder = Metadata.builder(clusterState.metadata())
+            .put(newInstance(featureDataStreamName, List.of(backingIndex.getIndex())));
+        ClusterState state = ClusterState.builder(clusterState).metadata(mdBuilder).build();
+
+        var service = createAllocationHealthIndicatorService(
+            Settings.EMPTY,
+            state,
+            Map.of(),
+            getSystemIndices(featureDataStreamName, ".test-ds-*", ".feature-*")
+        );
+        HealthIndicatorResult result = service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO);
+
+        assertThat(result.status(), is(HealthStatus.RED));
+        assertThat(result.diagnosisList().size(), is(1));
+        Diagnosis diagnosis = result.diagnosisList().get(0);
+        List<Diagnosis.Resource> affectedResources = diagnosis.affectedResources();
+        assertThat("expecting we report a resource of type INDEX and one of type FEATURE_STATE", affectedResources.size(), is(2));
+        for (Diagnosis.Resource resource : affectedResources) {
+            if (resource.getType() == INDEX) {
+                assertThat(resource.getValues(), hasItems("regular-index"));
+            } else {
+                assertThat(resource.getType(), is(FEATURE_STATE));
+                assertThat(resource.getValues(), hasItems("feature-with-system-data-stream", "feature-with-system-index"));
+            }
+        }
+    }
+
+    public void testGetRestoreFromSnapshotAffectedResources() {
+        String featureDataStreamName = ".test-ds-feature";
+        IndexMetadata backingIndex = createBackingIndex(featureDataStreamName, 1).build();
+
+        List<IndexMetadata> indexMetadataList = List.of(
+            IndexMetadata.builder(".feature-index")
+                .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build())
+                .numberOfShards(1)
+                .numberOfReplicas(0)
+                .build(),
+            IndexMetadata.builder("regular-index")
+                .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build())
+                .numberOfShards(1)
+                .numberOfReplicas(0)
+                .build(),
+            backingIndex
+        );
+
+        Metadata.Builder metadataBuilder = Metadata.builder();
+        Map<String, IndexMetadata> indexMetadataMap = new HashMap<>();
+        for (IndexMetadata indexMetadata : indexMetadataList) {
+            indexMetadataMap.put(indexMetadata.getIndex().getName(), indexMetadata);
+        }
+        metadataBuilder.indices(indexMetadataMap);
+        metadataBuilder.put(newInstance(featureDataStreamName, List.of(backingIndex.getIndex())));
+        Metadata metadata = metadataBuilder.build();
+        {
+            List<Diagnosis.Resource> affectedResources = ShardAllocationStatus.getRestoreFromSnapshotAffectedResources(
+                metadata,
+                getSystemIndices(featureDataStreamName, ".test-ds-*", ".feature-*"),
+                Set.of(backingIndex.getIndex().getName(), ".feature-index", "regular-index"),
+                10
+            );
+
+            assertThat(affectedResources.size(), is(2));
+            for (Diagnosis.Resource resource : affectedResources) {
+                if (resource.getType() == INDEX) {
+                    assertThat(resource.getValues(), hasItems("regular-index"));
+                } else {
+                    assertThat(resource.getType(), is(FEATURE_STATE));
+                    assertThat(resource.getValues(), hasItems("feature-with-system-data-stream", "feature-with-system-index"));
+                }
+            }
+        }
+
+        {
+            List<Diagnosis.Resource> affectedResources = ShardAllocationStatus.getRestoreFromSnapshotAffectedResources(
+                metadata,
+                getSystemIndices(featureDataStreamName, ".test-ds-*", ".feature-*"),
+                Set.of(backingIndex.getIndex().getName(), ".feature-index", "regular-index"),
+                0
+            );
+
+            assertThat(affectedResources.size(), is(2));
+            for (Diagnosis.Resource resource : affectedResources) {
+                if (resource.getType() == INDEX) {
+                    assertThat(resource.getValues(), emptyCollectionOf(String.class));
+                } else {
+                    assertThat(resource.getType(), is(FEATURE_STATE));
+                    assertThat(resource.getValues(), emptyCollectionOf(String.class));
+                }
+            }
+        }
+
+    }
+
     public void testDiagnoseUnknownAllocationDeciderIssue() {
         // Index definition, 1 primary no replicas
         IndexMetadata indexMetadata = IndexMetadata.builder("red-index")
@@ -1253,6 +1410,53 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
         }
     }
 
+    /**
+     * Creates the {@link SystemIndices} with one standalone system index and a system data stream
+     */
+    private SystemIndices getSystemIndices(
+        String featureDataStreamName,
+        String systemDataStreamPattern,
+        String standaloneSystemIndexPattern
+    ) {
+        return new SystemIndices(
+            List.of(
+                new SystemIndices.Feature(
+                    "feature-with-system-index",
+                    "testing",
+                    List.of(new SystemIndexDescriptor(standaloneSystemIndexPattern, "feature with index"))
+                ),
+                new SystemIndices.Feature(
+                    "feature-with-system-data-stream",
+                    "feature with data stream",
+                    List.of(),
+                    List.of(
+                        new SystemDataStreamDescriptor(
+                            featureDataStreamName,
+                            "description",
+                            SystemDataStreamDescriptor.Type.EXTERNAL,
+                            new ComposableIndexTemplate(
+                                List.of(systemDataStreamPattern),
+                                null,
+                                null,
+                                null,
+                                null,
+                                null,
+                                new ComposableIndexTemplate.DataStreamTemplate()
+                            ),
+                            Map.of(),
+                            List.of("test"),
+                            new ExecutorNames(
+                                ThreadPool.Names.SYSTEM_CRITICAL_READ,
+                                ThreadPool.Names.SYSTEM_READ,
+                                ThreadPool.Names.SYSTEM_WRITE
+                            )
+                        )
+                    )
+                )
+            )
+        );
+    }
+
     private HealthIndicatorResult createExpectedResult(
         HealthStatus status,
         String symptom,
@@ -1271,7 +1475,7 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
     }
 
     private HealthIndicatorResult createExpectedTruncatedResult(HealthStatus status, String symptom, List<HealthIndicatorImpact> impacts) {
-        return new HealthIndicatorResult(NAME, status, symptom, HealthIndicatorDetails.EMPTY, impacts, Collections.emptyList());
+        return new HealthIndicatorResult(NAME, status, symptom, HealthIndicatorDetails.EMPTY, impacts, emptyList());
     }
 
     private static ClusterState createClusterStateWith(List<IndexRoutingTable> indexRoutes, List<NodeShutdown> nodeShutdowns) {
@@ -1534,13 +1738,22 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
         ClusterState clusterState,
         final Map<ShardRoutingKey, ShardAllocationDecision> decisions
     ) {
-        return createShardsAvailabilityIndicatorService(Settings.EMPTY, clusterState, decisions);
+        return createAllocationHealthIndicatorService(Settings.EMPTY, clusterState, decisions, new SystemIndices(List.of()));
     }
 
     private static ShardsAvailabilityHealthIndicatorService createShardsAvailabilityIndicatorService(
         Settings nodeSettings,
         ClusterState clusterState,
         final Map<ShardRoutingKey, ShardAllocationDecision> decisions
+    ) {
+        return createAllocationHealthIndicatorService(nodeSettings, clusterState, decisions, new SystemIndices(List.of()));
+    }
+
+    private static ShardsAvailabilityHealthIndicatorService createAllocationHealthIndicatorService(
+        Settings nodeSettings,
+        ClusterState clusterState,
+        final Map<ShardRoutingKey, ShardAllocationDecision> decisions,
+        SystemIndices systemIndices
     ) {
         var clusterService = mock(ClusterService.class);
         when(clusterService.state()).thenReturn(clusterState);
@@ -1552,6 +1765,6 @@ public class ShardsAvailabilityHealthIndicatorServiceTests extends ESTestCase {
             var key = new ShardRoutingKey(shardRouting.getIndexName(), shardRouting.getId(), shardRouting.primary());
             return decisions.getOrDefault(key, ShardAllocationDecision.NOT_TAKEN);
         });
-        return new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService);
+        return new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService, systemIndices);
     }
 }