Jelajahi Sumber

Register Feature migration persistent task state named XContent (#84192)

This PR properly registers `NamedXContentRegistry` entries for `SystemIndexMigrationTaskParams` and `SystemIndexMigrationTaskState`. It also adds tests for the XContent de/serialization for those classes, 
and fixes a bug revealed by these tests in `SystemIndexMigrationTaskState`'s parser.

Finally, it adds an integration test which simulates the conditions in which #84115 occurs: A node restart 
while the migration is in progress. This ensures that we have fixed that particular bug.

Co-authored-by: David Turner <david.turner@elastic.co>
Co-authored-by: Gordon Brown <gordon.brown@elastic.co>
Przemyslaw Gomulka 3 tahun lalu
induk
melakukan
4c3b7b1fb1

+ 6 - 0
docs/changelog/84192.yaml

@@ -0,0 +1,6 @@
+pr: 84192
+summary: Registration of `SystemIndexMigrationTask` named xcontent objects
+area: Infra/Core
+type: bug
+issues:
+ - 84115

+ 274 - 0
modules/reindex/src/internalClusterTest/java/org/elasticsearch/migration/AbstractFeatureMigrationIntegTest.java

@@ -0,0 +1,274 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.migration;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
+import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
+import org.elasticsearch.action.admin.indices.stats.IndexStats;
+import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
+import org.elasticsearch.action.index.IndexRequestBuilder;
+import org.elasticsearch.action.support.ActiveShardCount;
+import org.elasticsearch.client.internal.Client;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.indices.AssociatedIndexDescriptor;
+import org.elasticsearch.indices.SystemIndexDescriptor;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.plugins.SystemIndexPlugin;
+import org.elasticsearch.test.ESIntegTestCase;
+import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.json.JsonXContent;
+import org.junit.Assert;
+import org.junit.Before;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+public abstract class AbstractFeatureMigrationIntegTest extends ESIntegTestCase {
+
+    static final String VERSION_META_KEY = "version";
+    static final Version META_VERSION = Version.CURRENT;
+    static final String DESCRIPTOR_MANAGED_META_KEY = "desciptor_managed";
+    static final String DESCRIPTOR_INTERNAL_META_KEY = "descriptor_internal";
+    static final String FEATURE_NAME = "A-test-feature"; // Sorts alphabetically before the feature from MultiFeatureMigrationIT
+    static final String ORIGIN = AbstractFeatureMigrationIntegTest.class.getSimpleName();
+    static final String FlAG_SETTING_KEY = IndexMetadata.INDEX_PRIORITY_SETTING.getKey();
+    static final String INTERNAL_MANAGED_INDEX_NAME = ".int-man-old";
+    static final int INDEX_DOC_COUNT = 100; // arbitrarily chosen
+    static final int INTERNAL_MANAGED_FLAG_VALUE = 1;
+    public static final Version NEEDS_UPGRADE_VERSION = Version.V_7_0_0;
+
+    static final SystemIndexDescriptor EXTERNAL_UNMANAGED = SystemIndexDescriptor.builder()
+        .setIndexPattern(".ext-unman-*")
+        .setType(SystemIndexDescriptor.Type.EXTERNAL_UNMANAGED)
+        .setOrigin(ORIGIN)
+        .setVersionMetaKey(VERSION_META_KEY)
+        .setAllowedElasticProductOrigins(Collections.singletonList(ORIGIN))
+        .setMinimumNodeVersion(NEEDS_UPGRADE_VERSION)
+        .setPriorSystemIndexDescriptors(Collections.emptyList())
+        .build();
+    static final SystemIndexDescriptor INTERNAL_UNMANAGED = SystemIndexDescriptor.builder()
+        .setIndexPattern(".int-unman-*")
+        .setType(SystemIndexDescriptor.Type.INTERNAL_UNMANAGED)
+        .setOrigin(ORIGIN)
+        .setVersionMetaKey(VERSION_META_KEY)
+        .setAllowedElasticProductOrigins(Collections.emptyList())
+        .setMinimumNodeVersion(NEEDS_UPGRADE_VERSION)
+        .setPriorSystemIndexDescriptors(Collections.emptyList())
+        .build();
+
+    static final SystemIndexDescriptor INTERNAL_MANAGED = SystemIndexDescriptor.builder()
+        .setIndexPattern(".int-man-*")
+        .setAliasName(".internal-managed-alias")
+        .setPrimaryIndex(INTERNAL_MANAGED_INDEX_NAME)
+        .setType(SystemIndexDescriptor.Type.INTERNAL_MANAGED)
+        .setSettings(createSimpleSettings(NEEDS_UPGRADE_VERSION, INTERNAL_MANAGED_FLAG_VALUE))
+        .setMappings(createSimpleMapping(true, true))
+        .setOrigin(ORIGIN)
+        .setVersionMetaKey(VERSION_META_KEY)
+        .setAllowedElasticProductOrigins(Collections.emptyList())
+        .setMinimumNodeVersion(NEEDS_UPGRADE_VERSION)
+        .setPriorSystemIndexDescriptors(Collections.emptyList())
+        .build();
+    static final int INTERNAL_UNMANAGED_FLAG_VALUE = 2;
+    static final int EXTERNAL_MANAGED_FLAG_VALUE = 3;
+    static final SystemIndexDescriptor EXTERNAL_MANAGED = SystemIndexDescriptor.builder()
+        .setIndexPattern(".ext-man-*")
+        .setAliasName(".external-managed-alias")
+        .setPrimaryIndex(".ext-man-old")
+        .setType(SystemIndexDescriptor.Type.EXTERNAL_MANAGED)
+        .setSettings(createSimpleSettings(NEEDS_UPGRADE_VERSION, EXTERNAL_MANAGED_FLAG_VALUE))
+        .setMappings(createSimpleMapping(true, false))
+        .setOrigin(ORIGIN)
+        .setVersionMetaKey(VERSION_META_KEY)
+        .setAllowedElasticProductOrigins(Collections.singletonList(ORIGIN))
+        .setMinimumNodeVersion(NEEDS_UPGRADE_VERSION)
+        .setPriorSystemIndexDescriptors(Collections.emptyList())
+        .build();
+    static final int EXTERNAL_UNMANAGED_FLAG_VALUE = 4;
+    static final String ASSOCIATED_INDEX_NAME = ".my-associated-idx";
+
+    @Before
+    public void setupTestPlugin() {
+        TestPlugin.preMigrationHook.set((state) -> Collections.emptyMap());
+        TestPlugin.postMigrationHook.set((state, metadata) -> {});
+    }
+
+    public void createSystemIndexForDescriptor(SystemIndexDescriptor descriptor) throws InterruptedException {
+        Assert.assertTrue(
+            "the strategy used below to create index names for descriptors without a primary index name only works for simple patterns",
+            descriptor.getIndexPattern().endsWith("*")
+        );
+        String indexName = Optional.ofNullable(descriptor.getPrimaryIndex()).orElse(descriptor.getIndexPattern().replace("*", "old"));
+        CreateIndexRequestBuilder createRequest = prepareCreate(indexName);
+        createRequest.setWaitForActiveShards(ActiveShardCount.ALL);
+        if (SystemIndexDescriptor.DEFAULT_SETTINGS.equals(descriptor.getSettings())) {
+            // unmanaged
+            createRequest.setSettings(
+                createSimpleSettings(
+                    NEEDS_UPGRADE_VERSION,
+                    descriptor.isInternal() ? INTERNAL_UNMANAGED_FLAG_VALUE : EXTERNAL_UNMANAGED_FLAG_VALUE
+                )
+            );
+        } else {
+            // managed
+            createRequest.setSettings(
+                Settings.builder()
+                    .put("index.version.created", Version.CURRENT)
+                    .put(IndexMetadata.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 0)
+                    .build()
+            );
+        }
+        if (descriptor.getMappings() == null) {
+            createRequest.setMapping(createSimpleMapping(false, descriptor.isInternal()));
+        }
+        CreateIndexResponse response = createRequest.get();
+        Assert.assertTrue(response.isShardsAcknowledged());
+
+        List<IndexRequestBuilder> docs = new ArrayList<>(INDEX_DOC_COUNT);
+        for (int i = 0; i < INDEX_DOC_COUNT; i++) {
+            docs.add(ESIntegTestCase.client().prepareIndex(indexName).setId(Integer.toString(i)).setSource("some_field", "words words"));
+        }
+        indexRandom(true, docs);
+        IndicesStatsResponse indexStats = ESIntegTestCase.client().admin().indices().prepareStats(indexName).setDocs(true).get();
+        Assert.assertThat(indexStats.getIndex(indexName).getTotal().getDocs().getCount(), is((long) INDEX_DOC_COUNT));
+    }
+
+    static Settings createSimpleSettings(Version creationVersion, int flagSettingValue) {
+        return Settings.builder()
+            .put(IndexMetadata.INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1)
+            .put(IndexMetadata.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 0)
+            .put(FlAG_SETTING_KEY, flagSettingValue)
+            .put("index.version.created", creationVersion)
+            .build();
+    }
+
+    static String createSimpleMapping(boolean descriptorManaged, boolean descriptorInternal) {
+        try (XContentBuilder builder = JsonXContent.contentBuilder()) {
+            builder.startObject();
+            {
+                builder.startObject("_meta");
+                builder.field(VERSION_META_KEY, META_VERSION);
+                builder.field(DESCRIPTOR_MANAGED_META_KEY, descriptorManaged);
+                builder.field(DESCRIPTOR_INTERNAL_META_KEY, descriptorInternal);
+                builder.endObject();
+
+                builder.field("dynamic", "strict");
+                builder.startObject("properties");
+                {
+                    builder.startObject("some_field");
+                    builder.field("type", "keyword");
+                    builder.endObject();
+                }
+                builder.endObject();
+            }
+            builder.endObject();
+            return Strings.toString(builder);
+        } catch (IOException e) {
+            // Just rethrow, it should be impossible for this to throw here
+            throw new AssertionError(e);
+        }
+    }
+
+    public void assertIndexHasCorrectProperties(
+        Metadata metadata,
+        String indexName,
+        int settingsFlagValue,
+        boolean isManaged,
+        boolean isInternal,
+        Collection<String> aliasNames
+    ) {
+        IndexMetadata imd = metadata.index(indexName);
+        assertThat(imd.getSettings().get(FlAG_SETTING_KEY), equalTo(Integer.toString(settingsFlagValue)));
+        final Map<String, Object> mapping = imd.mapping().getSourceAsMap();
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> meta = (Map<String, Object>) mapping.get("_meta");
+        assertThat(meta.get(DESCRIPTOR_MANAGED_META_KEY), is(isManaged));
+        assertThat(meta.get(DESCRIPTOR_INTERNAL_META_KEY), is(isInternal));
+
+        assertThat(imd.isSystem(), is(true));
+
+        Set<String> actualAliasNames = imd.getAliases().keySet();
+        assertThat(actualAliasNames, containsInAnyOrder(aliasNames.toArray()));
+
+        IndicesStatsResponse indexStats = client().admin().indices().prepareStats(imd.getIndex().getName()).setDocs(true).get();
+        assertNotNull(indexStats);
+        final IndexStats thisIndexStats = indexStats.getIndex(imd.getIndex().getName());
+        assertNotNull(thisIndexStats);
+        assertNotNull(thisIndexStats.getTotal());
+        assertNotNull(thisIndexStats.getTotal().getDocs());
+        assertThat(thisIndexStats.getTotal().getDocs().getCount(), is((long) INDEX_DOC_COUNT));
+    }
+
+    public static class TestPlugin extends Plugin implements SystemIndexPlugin {
+        public static final AtomicReference<Function<ClusterState, Map<String, Object>>> preMigrationHook = new AtomicReference<>();
+        public static final AtomicReference<BiConsumer<ClusterState, Map<String, Object>>> postMigrationHook = new AtomicReference<>();
+
+        public TestPlugin() {
+
+        }
+
+        @Override
+        public String getFeatureName() {
+            return FEATURE_NAME;
+        }
+
+        @Override
+        public String getFeatureDescription() {
+            return "a plugin for testing system index migration";
+        }
+
+        @Override
+        public Collection<SystemIndexDescriptor> getSystemIndexDescriptors(Settings settings) {
+            return Arrays.asList(INTERNAL_MANAGED, INTERNAL_UNMANAGED, EXTERNAL_MANAGED, EXTERNAL_UNMANAGED);
+        }
+
+        @Override
+        public Collection<AssociatedIndexDescriptor> getAssociatedIndexDescriptors() {
+
+            return Collections.singletonList(new AssociatedIndexDescriptor(ASSOCIATED_INDEX_NAME, TestPlugin.class.getCanonicalName()));
+        }
+
+        @Override
+        public void prepareForIndicesMigration(ClusterService clusterService, Client client, ActionListener<Map<String, Object>> listener) {
+            listener.onResponse(preMigrationHook.get().apply(clusterService.state()));
+        }
+
+        @Override
+        public void indicesMigrationComplete(
+            Map<String, Object> preUpgradeMetadata,
+            ClusterService clusterService,
+            Client client,
+            ActionListener<Boolean> listener
+        ) {
+            postMigrationHook.get().accept(clusterService.state(), preUpgradeMetadata);
+            listener.onResponse(true);
+        }
+    }
+}

+ 1 - 231
modules/reindex/src/internalClusterTest/java/org/elasticsearch/migration/FeatureMigrationIT.java

@@ -9,8 +9,6 @@
 package org.elasticsearch.migration;
 
 import org.apache.lucene.util.SetOnce;
-import org.elasticsearch.Version;
-import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusAction;
 import org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusRequest;
 import org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusResponse;
@@ -19,11 +17,7 @@ import org.elasticsearch.action.admin.cluster.migration.PostFeatureUpgradeReques
 import org.elasticsearch.action.admin.cluster.migration.PostFeatureUpgradeResponse;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
 import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
-import org.elasticsearch.action.admin.indices.stats.IndexStats;
-import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
-import org.elasticsearch.action.index.IndexRequestBuilder;
 import org.elasticsearch.action.support.ActiveShardCount;
-import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.ClusterStateTaskExecutor;
 import org.elasticsearch.cluster.ClusterStateUpdateTask;
@@ -32,18 +26,11 @@ import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.indices.AssociatedIndexDescriptor;
-import org.elasticsearch.indices.SystemIndexDescriptor;
 import org.elasticsearch.plugins.Plugin;
-import org.elasticsearch.plugins.SystemIndexPlugin;
 import org.elasticsearch.reindex.ReindexPlugin;
-import org.elasticsearch.test.ESIntegTestCase;
 import org.elasticsearch.upgrades.FeatureMigrationResults;
 import org.elasticsearch.upgrades.SingleFeatureMigrationResult;
-import org.elasticsearch.xcontent.XContentBuilder;
-import org.elasticsearch.xcontent.json.JsonXContent;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -55,14 +42,10 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.BiConsumer;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import static org.hamcrest.Matchers.aMapWithSize;
 import static org.hamcrest.Matchers.allOf;
-import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasEntry;
 import static org.hamcrest.Matchers.hasItem;
@@ -71,7 +54,7 @@ import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
 
-public class FeatureMigrationIT extends ESIntegTestCase {
+public class FeatureMigrationIT extends AbstractFeatureMigrationIntegTest {
     @Override
     protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) {
         return Settings.builder().put(super.nodeSettings(nodeOrdinal, otherSettings)).build();
@@ -332,217 +315,4 @@ public class FeatureMigrationIT extends ESIntegTestCase {
             assertThat(statusResp.getUpgradeStatus(), equalTo(GetFeatureUpgradeStatusResponse.UpgradeStatus.NO_MIGRATION_NEEDED));
         });
     }
-
-    public void assertIndexHasCorrectProperties(
-        Metadata metadata,
-        String indexName,
-        int settingsFlagValue,
-        boolean isManaged,
-        boolean isInternal,
-        Collection<String> aliasNames
-    ) {
-        IndexMetadata imd = metadata.index(indexName);
-        assertThat(imd.getSettings().get(FlAG_SETTING_KEY), equalTo(Integer.toString(settingsFlagValue)));
-        final Map<String, Object> mapping = imd.mapping().getSourceAsMap();
-        @SuppressWarnings("unchecked")
-        final Map<String, Object> meta = (Map<String, Object>) mapping.get("_meta");
-        assertThat(meta.get(DESCRIPTOR_MANAGED_META_KEY), is(isManaged));
-        assertThat(meta.get(DESCRIPTOR_INTERNAL_META_KEY), is(isInternal));
-
-        assertThat(imd.isSystem(), is(true));
-
-        Set<String> actualAliasNames = imd.getAliases().keySet();
-        assertThat(actualAliasNames, containsInAnyOrder(aliasNames.toArray()));
-
-        IndicesStatsResponse indexStats = client().admin().indices().prepareStats(imd.getIndex().getName()).setDocs(true).get();
-        assertNotNull(indexStats);
-        final IndexStats thisIndexStats = indexStats.getIndex(imd.getIndex().getName());
-        assertNotNull(thisIndexStats);
-        assertNotNull(thisIndexStats.getTotal());
-        assertNotNull(thisIndexStats.getTotal().getDocs());
-        assertThat(thisIndexStats.getTotal().getDocs().getCount(), is((long) INDEX_DOC_COUNT));
-    }
-
-    public void createSystemIndexForDescriptor(SystemIndexDescriptor descriptor) throws InterruptedException {
-        assertTrue(
-            "the strategy used below to create index names for descriptors without a primary index name only works for simple patterns",
-            descriptor.getIndexPattern().endsWith("*")
-        );
-        String indexName = Optional.ofNullable(descriptor.getPrimaryIndex()).orElse(descriptor.getIndexPattern().replace("*", "old"));
-        CreateIndexRequestBuilder createRequest = prepareCreate(indexName);
-        createRequest.setWaitForActiveShards(ActiveShardCount.ALL);
-        if (SystemIndexDescriptor.DEFAULT_SETTINGS.equals(descriptor.getSettings())) {
-            // unmanaged
-            createRequest.setSettings(
-                createSimpleSettings(
-                    NEEDS_UPGRADE_VERSION,
-                    descriptor.isInternal() ? INTERNAL_UNMANAGED_FLAG_VALUE : EXTERNAL_UNMANAGED_FLAG_VALUE
-                )
-            );
-        } else {
-            // managed
-            createRequest.setSettings(
-                Settings.builder()
-                    .put("index.version.created", Version.CURRENT)
-                    .put(IndexMetadata.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 0)
-                    .build()
-            );
-        }
-        if (descriptor.getMappings() == null) {
-            createRequest.setMapping(createSimpleMapping(false, descriptor.isInternal()));
-        }
-        CreateIndexResponse response = createRequest.get();
-        assertTrue(response.isShardsAcknowledged());
-
-        List<IndexRequestBuilder> docs = new ArrayList<>(INDEX_DOC_COUNT);
-        for (int i = 0; i < INDEX_DOC_COUNT; i++) {
-            docs.add(client().prepareIndex(indexName).setId(Integer.toString(i)).setSource("some_field", "words words"));
-        }
-        indexRandom(true, docs);
-        IndicesStatsResponse indexStats = client().admin().indices().prepareStats(indexName).setDocs(true).get();
-        assertThat(indexStats.getIndex(indexName).getTotal().getDocs().getCount(), is((long) INDEX_DOC_COUNT));
-    }
-
-    static final String VERSION_META_KEY = "version";
-    static final Version META_VERSION = Version.CURRENT;
-    static final String DESCRIPTOR_MANAGED_META_KEY = "desciptor_managed";
-    static final String DESCRIPTOR_INTERNAL_META_KEY = "descriptor_internal";
-    static final String FEATURE_NAME = "A-test-feature"; // Sorts alphabetically before the feature from MultiFeatureMigrationIT
-    static final String ORIGIN = FeatureMigrationIT.class.getSimpleName();
-    static final String FlAG_SETTING_KEY = IndexMetadata.INDEX_PRIORITY_SETTING.getKey();
-    static final String INTERNAL_MANAGED_INDEX_NAME = ".int-man-old";
-    static final int INDEX_DOC_COUNT = 100; // arbitrarily chosen
-    public static final Version NEEDS_UPGRADE_VERSION = Version.V_7_0_0;
-
-    static final int INTERNAL_MANAGED_FLAG_VALUE = 1;
-    static final int INTERNAL_UNMANAGED_FLAG_VALUE = 2;
-    static final int EXTERNAL_MANAGED_FLAG_VALUE = 3;
-    static final int EXTERNAL_UNMANAGED_FLAG_VALUE = 4;
-    static final SystemIndexDescriptor INTERNAL_MANAGED = SystemIndexDescriptor.builder()
-        .setIndexPattern(".int-man-*")
-        .setAliasName(".internal-managed-alias")
-        .setPrimaryIndex(INTERNAL_MANAGED_INDEX_NAME)
-        .setType(SystemIndexDescriptor.Type.INTERNAL_MANAGED)
-        .setSettings(createSimpleSettings(NEEDS_UPGRADE_VERSION, INTERNAL_MANAGED_FLAG_VALUE))
-        .setMappings(createSimpleMapping(true, true))
-        .setOrigin(ORIGIN)
-        .setVersionMetaKey(VERSION_META_KEY)
-        .setAllowedElasticProductOrigins(Collections.emptyList())
-        .setMinimumNodeVersion(NEEDS_UPGRADE_VERSION)
-        .setPriorSystemIndexDescriptors(Collections.emptyList())
-        .build();
-    static final SystemIndexDescriptor INTERNAL_UNMANAGED = SystemIndexDescriptor.builder()
-        .setIndexPattern(".int-unman-*")
-        .setType(SystemIndexDescriptor.Type.INTERNAL_UNMANAGED)
-        .setOrigin(ORIGIN)
-        .setVersionMetaKey(VERSION_META_KEY)
-        .setAllowedElasticProductOrigins(Collections.emptyList())
-        .setMinimumNodeVersion(NEEDS_UPGRADE_VERSION)
-        .setPriorSystemIndexDescriptors(Collections.emptyList())
-        .build();
-    static final SystemIndexDescriptor EXTERNAL_MANAGED = SystemIndexDescriptor.builder()
-        .setIndexPattern(".ext-man-*")
-        .setAliasName(".external-managed-alias")
-        .setPrimaryIndex(".ext-man-old")
-        .setType(SystemIndexDescriptor.Type.EXTERNAL_MANAGED)
-        .setSettings(createSimpleSettings(NEEDS_UPGRADE_VERSION, EXTERNAL_MANAGED_FLAG_VALUE))
-        .setMappings(createSimpleMapping(true, false))
-        .setOrigin(ORIGIN)
-        .setVersionMetaKey(VERSION_META_KEY)
-        .setAllowedElasticProductOrigins(Collections.singletonList(ORIGIN))
-        .setMinimumNodeVersion(NEEDS_UPGRADE_VERSION)
-        .setPriorSystemIndexDescriptors(Collections.emptyList())
-        .build();
-    static final SystemIndexDescriptor EXTERNAL_UNMANAGED = SystemIndexDescriptor.builder()
-        .setIndexPattern(".ext-unman-*")
-        .setType(SystemIndexDescriptor.Type.EXTERNAL_UNMANAGED)
-        .setOrigin(ORIGIN)
-        .setVersionMetaKey(VERSION_META_KEY)
-        .setAllowedElasticProductOrigins(Collections.singletonList(ORIGIN))
-        .setMinimumNodeVersion(NEEDS_UPGRADE_VERSION)
-        .setPriorSystemIndexDescriptors(Collections.emptyList())
-        .build();
-    static final String ASSOCIATED_INDEX_NAME = ".my-associated-idx";
-
-    static Settings createSimpleSettings(Version creationVersion, int flagSettingValue) {
-        return Settings.builder()
-            .put(IndexMetadata.INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1)
-            .put(IndexMetadata.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 0)
-            .put(FlAG_SETTING_KEY, flagSettingValue)
-            .put("index.version.created", creationVersion)
-            .build();
-    }
-
-    static String createSimpleMapping(boolean descriptorManaged, boolean descriptorInternal) {
-        try (XContentBuilder builder = JsonXContent.contentBuilder()) {
-            builder.startObject();
-            {
-                builder.startObject("_meta");
-                builder.field(VERSION_META_KEY, META_VERSION);
-                builder.field(DESCRIPTOR_MANAGED_META_KEY, descriptorManaged);
-                builder.field(DESCRIPTOR_INTERNAL_META_KEY, descriptorInternal);
-                builder.endObject();
-
-                builder.field("dynamic", "strict");
-                builder.startObject("properties");
-                {
-                    builder.startObject("some_field");
-                    builder.field("type", "keyword");
-                    builder.endObject();
-                }
-                builder.endObject();
-            }
-            builder.endObject();
-            return Strings.toString(builder);
-        } catch (IOException e) {
-            // Just rethrow, it should be impossible for this to throw here
-            throw new AssertionError(e);
-        }
-    }
-
-    public static class TestPlugin extends Plugin implements SystemIndexPlugin {
-        public static final AtomicReference<Function<ClusterState, Map<String, Object>>> preMigrationHook = new AtomicReference<>();
-        public static final AtomicReference<BiConsumer<ClusterState, Map<String, Object>>> postMigrationHook = new AtomicReference<>();
-
-        public TestPlugin() {
-
-        }
-
-        @Override
-        public String getFeatureName() {
-            return FEATURE_NAME;
-        }
-
-        @Override
-        public String getFeatureDescription() {
-            return "a plugin for testing system index migration";
-        }
-
-        @Override
-        public Collection<SystemIndexDescriptor> getSystemIndexDescriptors(Settings settings) {
-            return Arrays.asList(INTERNAL_MANAGED, INTERNAL_UNMANAGED, EXTERNAL_MANAGED, EXTERNAL_UNMANAGED);
-        }
-
-        @Override
-        public Collection<AssociatedIndexDescriptor> getAssociatedIndexDescriptors() {
-
-            return Collections.singletonList(new AssociatedIndexDescriptor(ASSOCIATED_INDEX_NAME, TestPlugin.class.getCanonicalName()));
-        }
-
-        @Override
-        public void prepareForIndicesMigration(ClusterService clusterService, Client client, ActionListener<Map<String, Object>> listener) {
-            listener.onResponse(preMigrationHook.get().apply(clusterService.state()));
-        }
-
-        @Override
-        public void indicesMigrationComplete(
-            Map<String, Object> preUpgradeMetadata,
-            ClusterService clusterService,
-            Client client,
-            ActionListener<Boolean> listener
-        ) {
-            postMigrationHook.get().accept(clusterService.state(), preUpgradeMetadata);
-            listener.onResponse(true);
-        }
-    }
 }

+ 2 - 2
modules/reindex/src/internalClusterTest/java/org/elasticsearch/migration/MultiFeatureMigrationIT.java

@@ -54,7 +54,7 @@ import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
 
-public class MultiFeatureMigrationIT extends FeatureMigrationIT {
+public class MultiFeatureMigrationIT extends AbstractFeatureMigrationIntegTest {
 
     @Override
     protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) {
@@ -70,7 +70,7 @@ public class MultiFeatureMigrationIT extends FeatureMigrationIT {
     @Override
     protected Collection<Class<? extends Plugin>> nodePlugins() {
         List<Class<? extends Plugin>> plugins = new ArrayList<>(super.nodePlugins());
-        plugins.add(FeatureMigrationIT.TestPlugin.class);
+        plugins.add(TestPlugin.class);
         plugins.add(SecondPlugin.class);
         plugins.add(ReindexPlugin.class);
         return plugins;

+ 126 - 0
modules/reindex/src/internalClusterTest/java/org/elasticsearch/migration/SystemIndexMigrationIT.java

@@ -0,0 +1,126 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.migration;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusAction;
+import org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusRequest;
+import org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusResponse;
+import org.elasticsearch.action.admin.cluster.migration.PostFeatureUpgradeAction;
+import org.elasticsearch.action.admin.cluster.migration.PostFeatureUpgradeRequest;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.ClusterStateListener;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.persistent.PersistentTasksCustomMetadata;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.reindex.ReindexPlugin;
+import org.elasticsearch.test.ESIntegTestCase;
+import org.elasticsearch.test.InternalTestCluster;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.elasticsearch.upgrades.SystemIndexMigrationTaskParams.SYSTEM_INDEX_UPGRADE_TASK_NAME;
+import static org.hamcrest.Matchers.equalTo;
+
+/**
+ * This class is for testing that when restarting a node, SystemIndexMigrationTaskState can be read.
+ */
+@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0, numClientNodes = 0, autoManageMasterNodes = false)
+public class SystemIndexMigrationIT extends AbstractFeatureMigrationIntegTest {
+    private static Logger logger = LogManager.getLogger(SystemIndexMigrationIT.class);
+
+    @Override
+    protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) {
+        return Settings.builder().put(super.nodeSettings(nodeOrdinal, otherSettings)).build();
+    }
+
+    @Override
+    protected boolean forbidPrivateIndexSettings() {
+        // We need to be able to set the index creation version manually.
+        return false;
+    }
+
+    @Override
+    protected Collection<Class<? extends Plugin>> nodePlugins() {
+        List<Class<? extends Plugin>> plugins = new ArrayList<>(super.nodePlugins());
+        plugins.add(TestPlugin.class);
+        plugins.add(ReindexPlugin.class);
+        return plugins;
+    }
+
+    public void testSystemIndexMigrationCanBeInterruptedWithShutdown() throws Exception {
+
+        CyclicBarrier taskCreated = new CyclicBarrier(2);
+        CyclicBarrier shutdownCompleted = new CyclicBarrier(2);
+        AtomicBoolean hasBlocked = new AtomicBoolean();
+
+        internalCluster().setBootstrapMasterNodeIndex(0);
+        final String masterName = internalCluster().startMasterOnlyNode();
+        final String masterAndDataNode = internalCluster().startNode();
+        createSystemIndexForDescriptor(INTERNAL_MANAGED);
+
+        final ClusterStateListener clusterStateListener = event -> {
+
+            if (PersistentTasksCustomMetadata.getTaskWithId(event.state(), SYSTEM_INDEX_UPGRADE_TASK_NAME) != null
+                && hasBlocked.compareAndSet(false, true)) {
+                try {
+                    logger.info("Task created");
+                    taskCreated.await(10, TimeUnit.SECONDS); // now we can test internalCluster().restartNode
+
+                    shutdownCompleted.await(10, TimeUnit.SECONDS); // waiting until the node has stopped
+                } catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
+                    throw new AssertionError(e);
+                }
+            }
+
+        };
+        final ClusterService clusterService = internalCluster().getCurrentMasterNodeInstance(ClusterService.class);
+        clusterService.addListener(clusterStateListener);
+
+        // create task by calling API
+        final PostFeatureUpgradeRequest req = new PostFeatureUpgradeRequest();
+        client().execute(PostFeatureUpgradeAction.INSTANCE, req);
+        logger.info("migrate feature api called");
+
+        taskCreated.await(10, TimeUnit.SECONDS); // waiting when the task is created
+
+        internalCluster().restartNode(masterAndDataNode, new InternalTestCluster.RestartCallback() {
+            @Override
+            public Settings onNodeStopped(String nodeName) throws Exception {
+                shutdownCompleted.await(10, TimeUnit.SECONDS);// now we can release the master thread
+                return super.onNodeStopped(nodeName);
+            }
+        });
+
+        assertBusy(() -> {
+            // Wait for the node we restarted to completely rejoin the cluster
+            ClusterState clusterState = client().admin().cluster().prepareState().get().getState();
+            assertThat("expected restarted node to rejoin cluster", clusterState.getNodes().size(), equalTo(2));
+
+            GetFeatureUpgradeStatusResponse statusResponse = client().execute(
+                GetFeatureUpgradeStatusAction.INSTANCE,
+                new GetFeatureUpgradeStatusRequest()
+            ).get();
+            assertThat(
+                "expected migration to fail due to restarting only data node",
+                statusResponse.getUpgradeStatus(),
+                equalTo(GetFeatureUpgradeStatusResponse.UpgradeStatus.ERROR)
+            );
+        });
+    }
+}

+ 2 - 1
server/src/main/java/org/elasticsearch/node/Node.java

@@ -498,7 +498,8 @@ public class Node implements Closeable {
                     IndicesModule.getNamedXContents().stream(),
                     searchModule.getNamedXContents().stream(),
                     pluginsService.filterPlugins(Plugin.class).stream().flatMap(p -> p.getNamedXContent().stream()),
-                    ClusterModule.getNamedXWriteables().stream()
+                    ClusterModule.getNamedXWriteables().stream(),
+                    SystemIndexMigrationExecutor.getNamedXContentParsers().stream()
                 ).flatMap(Function.identity()).collect(toList())
             );
             final Map<String, SystemIndices.Feature> featuresMap = pluginsService.filterPlugins(SystemIndexPlugin.class)

+ 17 - 0
server/src/main/java/org/elasticsearch/upgrades/SystemIndexMigrationExecutor.java

@@ -24,6 +24,8 @@ import org.elasticsearch.persistent.PersistentTasksCustomMetadata;
 import org.elasticsearch.persistent.PersistentTasksExecutor;
 import org.elasticsearch.tasks.TaskId;
 import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.xcontent.NamedXContentRegistry;
+import org.elasticsearch.xcontent.ParseField;
 
 import java.util.Collection;
 import java.util.List;
@@ -109,6 +111,21 @@ public class SystemIndexMigrationExecutor extends PersistentTasksExecutor<System
         }
     }
 
+    public static List<NamedXContentRegistry.Entry> getNamedXContentParsers() {
+        return List.of(
+            new NamedXContentRegistry.Entry(
+                PersistentTaskParams.class,
+                new ParseField(SystemIndexMigrationTaskParams.SYSTEM_INDEX_UPGRADE_TASK_NAME),
+                SystemIndexMigrationTaskParams::fromXContent
+            ),
+            new NamedXContentRegistry.Entry(
+                PersistentTaskState.class,
+                new ParseField(SystemIndexMigrationTaskParams.SYSTEM_INDEX_UPGRADE_TASK_NAME),
+                SystemIndexMigrationTaskState::fromXContent
+            )
+        );
+    }
+
     public static List<NamedWriteableRegistry.Entry> getNamedWriteables() {
         return List.of(
             new NamedWriteableRegistry.Entry(PersistentTaskState.class, SYSTEM_INDEX_UPGRADE_TASK_NAME, SystemIndexMigrationTaskState::new),

+ 5 - 0
server/src/main/java/org/elasticsearch/upgrades/SystemIndexMigrationTaskParams.java

@@ -14,6 +14,7 @@ import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.persistent.PersistentTaskParams;
 import org.elasticsearch.xcontent.ObjectParser;
 import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
 
@@ -37,6 +38,10 @@ public class SystemIndexMigrationTaskParams implements PersistentTaskParams {
         // PARSER.declareString etc...
     }
 
+    public static SystemIndexMigrationTaskParams fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
     public SystemIndexMigrationTaskParams() {
         // This space intentionally left blank
     }

+ 16 - 2
server/src/main/java/org/elasticsearch/upgrades/SystemIndexMigrationTaskState.java

@@ -36,10 +36,10 @@ public class SystemIndexMigrationTaskState implements PersistentTaskState {
     private static final ParseField FEATURE_METADATA_MAP_FIELD = new ParseField("feature_metadata");
 
     @SuppressWarnings(value = "unchecked")
-    private static final ConstructingObjectParser<SystemIndexMigrationTaskState, Void> PARSER = new ConstructingObjectParser<>(
+    static final ConstructingObjectParser<SystemIndexMigrationTaskState, Void> PARSER = new ConstructingObjectParser<>(
         SYSTEM_INDEX_UPGRADE_TASK_NAME,
         true,
-        args -> new SystemIndexMigrationTaskState((String) args[0], (String) args[1], (Map<String, Object>) args[3])
+        args -> new SystemIndexMigrationTaskState((String) args[0], (String) args[1], (Map<String, Object>) args[2])
     );
 
     static {
@@ -130,4 +130,18 @@ public class SystemIndexMigrationTaskState implements PersistentTaskState {
     public int hashCode() {
         return Objects.hash(currentIndex, currentFeature, featureCallbackMetadata);
     }
+
+    @Override
+    public String toString() {
+        return "SystemIndexMigrationTaskState{"
+            + "currentIndex='"
+            + currentIndex
+            + '\''
+            + ", currentFeature='"
+            + currentFeature
+            + '\''
+            + ", featureCallbackMetadata="
+            + featureCallbackMetadata
+            + '}';
+    }
 }

+ 1 - 10
server/src/test/java/org/elasticsearch/upgrades/SystemIndexMigrationTaskParamsTests.java

@@ -13,7 +13,6 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.test.AbstractNamedWriteableTestCase;
 
 import java.io.IOException;
-import java.util.Collections;
 
 public class SystemIndexMigrationTaskParamsTests extends AbstractNamedWriteableTestCase<SystemIndexMigrationTaskParams> {
 
@@ -32,15 +31,7 @@ public class SystemIndexMigrationTaskParamsTests extends AbstractNamedWriteableT
 
     @Override
     protected NamedWriteableRegistry getNamedWriteableRegistry() {
-        return new NamedWriteableRegistry(
-            Collections.singletonList(
-                new NamedWriteableRegistry.Entry(
-                    SystemIndexMigrationTaskParams.class,
-                    SystemIndexMigrationTaskParams.SYSTEM_INDEX_UPGRADE_TASK_NAME,
-                    SystemIndexMigrationTaskParams::new
-                )
-            )
-        );
+        return new NamedWriteableRegistry(SystemIndexMigrationExecutor.getNamedWriteables());
     }
 
     @Override

+ 38 - 0
server/src/test/java/org/elasticsearch/upgrades/SystemIndexMigrationTaskParamsXContentTests.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.upgrades;
+
+import org.elasticsearch.test.AbstractXContentTestCase;
+import org.elasticsearch.xcontent.NamedXContentRegistry;
+import org.elasticsearch.xcontent.XContentParser;
+
+import java.io.IOException;
+
+public class SystemIndexMigrationTaskParamsXContentTests extends AbstractXContentTestCase<SystemIndexMigrationTaskParams> {
+
+    @Override
+    protected SystemIndexMigrationTaskParams createTestInstance() {
+        return new SystemIndexMigrationTaskParams();
+    }
+
+    @Override
+    protected SystemIndexMigrationTaskParams doParseInstance(XContentParser parser) throws IOException {
+        return SystemIndexMigrationTaskParams.PARSER.parse(parser, null);
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return false;
+    }
+
+    @Override
+    protected NamedXContentRegistry xContentRegistry() {
+        return new NamedXContentRegistry(SystemIndexMigrationExecutor.getNamedXContentParsers());
+    }
+}

+ 16 - 19
server/src/test/java/org/elasticsearch/upgrades/SystemIndexMigrationTaskStateTests.java

@@ -14,13 +14,16 @@ import org.elasticsearch.core.Tuple;
 import org.elasticsearch.test.AbstractNamedWriteableTestCase;
 
 import java.io.IOException;
-import java.util.Collections;
 import java.util.Map;
 
 public class SystemIndexMigrationTaskStateTests extends AbstractNamedWriteableTestCase<SystemIndexMigrationTaskState> {
 
     @Override
     protected SystemIndexMigrationTaskState createTestInstance() {
+        return randomSystemIndexMigrationTask();
+    }
+
+    static SystemIndexMigrationTaskState randomSystemIndexMigrationTask() {
         return new SystemIndexMigrationTaskState(
             randomAlphaOfLengthBetween(5, 20),
             randomAlphaOfLengthBetween(5, 20),
@@ -28,27 +31,23 @@ public class SystemIndexMigrationTaskStateTests extends AbstractNamedWriteableTe
         );
     }
 
-    private Map<String, Object> randomFeatureCallbackMetadata() {
+    private static Map<String, Object> randomFeatureCallbackMetadata() {
         return randomMap(0, 10, () -> new Tuple<>(randomAlphaOfLength(5), randomMetadataValue()));
     }
 
-    private Object randomMetadataValue() {
-        switch (randomIntBetween(0, 7)) {
+    private static Object randomMetadataValue() {
+        switch (randomIntBetween(0, 5)) {
             case 0:
                 return randomMap(0, 3, () -> new Tuple<>(randomAlphaOfLength(5), randomMetadataValue()));
             case 1:
-                return randomList(0, 3, this::randomMetadataValue);
+                return randomList(0, 3, SystemIndexMigrationTaskStateTests::randomMetadataValue);
             case 2:
                 return randomLong();
             case 3:
-                return randomShort();
-            case 4:
                 return randomBoolean();
-            case 5:
-                return randomFloat();
-            case 6:
+            case 4:
                 return randomDouble();
-            case 7:
+            case 5:
                 return randomAlphaOfLengthBetween(5, 10);
         }
         throw new AssertionError("bad randomization");
@@ -76,7 +75,10 @@ public class SystemIndexMigrationTaskStateTests extends AbstractNamedWriteableTe
                 feature = randomValueOtherThan(instance.getCurrentFeature(), () -> randomAlphaOfLengthBetween(5, 20));
                 break;
             case 2:
-                featureMetadata = randomValueOtherThan(instance.getFeatureCallbackMetadata(), this::randomFeatureCallbackMetadata);
+                featureMetadata = randomValueOtherThan(
+                    instance.getFeatureCallbackMetadata(),
+                    SystemIndexMigrationTaskStateTests::randomFeatureCallbackMetadata
+                );
                 break;
             default:
                 assert false : "invalid randomization case";
@@ -87,13 +89,8 @@ public class SystemIndexMigrationTaskStateTests extends AbstractNamedWriteableTe
     @Override
     protected NamedWriteableRegistry getNamedWriteableRegistry() {
         return new NamedWriteableRegistry(
-            Collections.singletonList(
-                new NamedWriteableRegistry.Entry(
-                    SystemIndexMigrationTaskState.class,
-                    SystemIndexMigrationTaskParams.SYSTEM_INDEX_UPGRADE_TASK_NAME,
-                    SystemIndexMigrationTaskState::new
-                )
-            )
+            SystemIndexMigrationExecutor.getNamedWriteables()
+
         );
     }
 

+ 38 - 0
server/src/test/java/org/elasticsearch/upgrades/SystemIndexMigrationTaskStateXContentTests.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.upgrades;
+
+import org.elasticsearch.test.AbstractXContentTestCase;
+import org.elasticsearch.xcontent.NamedXContentRegistry;
+import org.elasticsearch.xcontent.XContentParser;
+
+import java.io.IOException;
+
+public class SystemIndexMigrationTaskStateXContentTests extends AbstractXContentTestCase<SystemIndexMigrationTaskState> {
+
+    @Override
+    protected SystemIndexMigrationTaskState createTestInstance() {
+        return SystemIndexMigrationTaskStateTests.randomSystemIndexMigrationTask();
+    }
+
+    @Override
+    protected SystemIndexMigrationTaskState doParseInstance(XContentParser parser) throws IOException {
+        return SystemIndexMigrationTaskState.PARSER.parse(parser, null);
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return false;
+    }
+
+    @Override
+    protected NamedXContentRegistry xContentRegistry() {
+        return new NamedXContentRegistry(SystemIndexMigrationExecutor.getNamedXContentParsers());
+    }
+}