Browse Source

Allow project reserved state handlers to update any cluster state (#128636)

Mark Vieira 4 months ago
parent
commit
8fce15d067
51 changed files with 573 additions and 458 deletions
  1. 2 1
      server/src/main/java/module-info.java
  2. 3 4
      server/src/main/java/org/elasticsearch/action/ActionModule.java
  3. 3 4
      server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/reservedstate/ReservedRepositoryAction.java
  4. 11 5
      server/src/main/java/org/elasticsearch/action/admin/indices/template/reservedstate/ReservedComposableIndexTemplateAction.java
  5. 13 11
      server/src/main/java/org/elasticsearch/action/ingest/ReservedPipelineAction.java
  6. 9 9
      server/src/main/java/org/elasticsearch/node/NodeConstruction.java
  7. 4 101
      server/src/main/java/org/elasticsearch/reservedstate/ReservedClusterStateHandler.java
  8. 40 0
      server/src/main/java/org/elasticsearch/reservedstate/ReservedProjectStateHandler.java
  9. 116 0
      server/src/main/java/org/elasticsearch/reservedstate/ReservedStateHandler.java
  10. 3 6
      server/src/main/java/org/elasticsearch/reservedstate/ReservedStateHandlerProvider.java
  11. 5 5
      server/src/main/java/org/elasticsearch/reservedstate/TransformState.java
  12. 3 4
      server/src/main/java/org/elasticsearch/reservedstate/action/ReservedClusterSettingsAction.java
  13. 6 17
      server/src/main/java/org/elasticsearch/reservedstate/service/ProjectClusterStateHandlerAdapter.java
  14. 77 27
      server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java
  15. 11 4
      server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateUpdateTask.java
  16. 17 7
      server/src/main/java/org/elasticsearch/reservedstate/service/ReservedProjectStateUpdateTask.java
  17. 10 12
      server/src/main/java/org/elasticsearch/reservedstate/service/ReservedStateUpdateTask.java
  18. 5 6
      server/src/test/java/org/elasticsearch/action/admin/cluster/repositories/reservedstate/ReservedRepositoryActionTests.java
  19. 30 36
      server/src/test/java/org/elasticsearch/action/admin/indices/template/reservedstate/ReservedComposableIndexTemplateActionTests.java
  20. 16 13
      server/src/test/java/org/elasticsearch/action/ingest/ReservedPipelineActionTests.java
  21. 2 2
      server/src/test/java/org/elasticsearch/reservedstate/ReservedClusterStateHandlerTests.java
  22. 7 11
      server/src/test/java/org/elasticsearch/reservedstate/action/ReservedClusterSettingsActionTests.java
  23. 52 44
      server/src/test/java/org/elasticsearch/reservedstate/service/ReservedClusterStateServiceTests.java
  24. 4 5
      x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/LocalStateReservedAutoscalingStateHandlerProvider.java
  25. 0 0
      x-pack/plugin/autoscaling/src/internalClusterTest/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider
  26. 3 3
      x-pack/plugin/autoscaling/src/main/java/module-info.java
  27. 1 2
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/Autoscaling.java
  28. 4 5
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/ReservedAutoscalingStateHandlerProvider.java
  29. 3 6
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/ReservedAutoscalingPolicyAction.java
  30. 0 0
      x-pack/plugin/autoscaling/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider
  31. 4 8
      x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/action/ReservedAutoscalingPolicyTests.java
  32. 3 3
      x-pack/plugin/ilm/src/main/java/module-info.java
  33. 2 3
      x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java
  34. 6 7
      x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ReservedLifecycleStateHandlerProvider.java
  35. 7 5
      x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/ReservedLifecycleAction.java
  36. 0 0
      x-pack/plugin/ilm/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider
  37. 30 24
      x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/ReservedLifecycleStateServiceTests.java
  38. 3 3
      x-pack/plugin/security/src/main/java/module-info.java
  39. 5 6
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ReservedSecurityStateHandlerProvider.java
  40. 2 3
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
  41. 9 7
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/rolemapping/ReservedRoleMappingAction.java
  42. 0 0
      x-pack/plugin/security/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider
  43. 5 6
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalReservedSecurityStateHandlerProvider.java
  44. 22 14
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/reservedstate/ReservedRoleMappingActionTests.java
  45. 0 0
      x-pack/plugin/security/src/test/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider
  46. 3 3
      x-pack/plugin/slm/src/main/java/module-info.java
  47. 4 5
      x-pack/plugin/slm/src/main/java/org/elasticsearch/xpack/slm/ReservedLifecycleStateHandlerProvider.java
  48. 1 2
      x-pack/plugin/slm/src/main/java/org/elasticsearch/xpack/slm/SnapshotLifecycle.java
  49. 3 4
      x-pack/plugin/slm/src/main/java/org/elasticsearch/xpack/slm/action/ReservedSnapshotAction.java
  50. 0 0
      x-pack/plugin/slm/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider
  51. 4 5
      x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/action/ReservedSnapshotLifecycleStateServiceTests.java

+ 2 - 1
server/src/main/java/module-info.java

@@ -8,6 +8,7 @@
  */
 
 import org.elasticsearch.plugins.internal.RestExtension;
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
 
 /** The Elasticsearch Server Module. */
 module org.elasticsearch.server {
@@ -411,7 +412,7 @@ module org.elasticsearch.server {
             org.elasticsearch.index.shard.ShardToolCliProvider;
 
     uses org.elasticsearch.reservedstate.service.FileSettingsServiceProvider;
-    uses org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider;
+    uses ReservedStateHandlerProvider;
     uses org.elasticsearch.jdk.ModuleQualifiedExportsService;
     uses org.elasticsearch.node.internal.TerminationHandlerProvider;
     uses org.elasticsearch.internal.VersionExtension;

+ 3 - 4
server/src/main/java/org/elasticsearch/action/ActionModule.java

@@ -211,9 +211,7 @@ import org.elasticsearch.action.termvectors.TransportShardMultiTermsVectorAction
 import org.elasticsearch.action.termvectors.TransportTermVectorsAction;
 import org.elasticsearch.action.update.TransportUpdateAction;
 import org.elasticsearch.client.internal.node.NodeClient;
-import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
-import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.cluster.node.DiscoveryNodes;
 import org.elasticsearch.cluster.project.ProjectIdResolver;
 import org.elasticsearch.cluster.routing.RerouteService;
@@ -254,6 +252,7 @@ import org.elasticsearch.plugins.internal.RestExtension;
 import org.elasticsearch.repositories.VerifyNodeRepositoryAction;
 import org.elasticsearch.repositories.VerifyNodeRepositoryCoordinationAction;
 import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
 import org.elasticsearch.reservedstate.service.ReservedClusterStateService;
 import org.elasticsearch.rest.RestController;
 import org.elasticsearch.rest.RestHandler;
@@ -474,8 +473,8 @@ public class ActionModule extends AbstractModule {
         TelemetryProvider telemetryProvider,
         ClusterService clusterService,
         RerouteService rerouteService,
-        List<ReservedClusterStateHandler<ClusterState, ?>> reservedClusterStateHandlers,
-        List<ReservedClusterStateHandler<ProjectMetadata, ?>> reservedProjectStateHandlers,
+        List<ReservedClusterStateHandler<?>> reservedClusterStateHandlers,
+        List<ReservedProjectStateHandler<?>> reservedProjectStateHandlers,
         RestExtension restExtension,
         IncrementalBulkService bulkService,
         ProjectIdResolver projectIdResolver

+ 3 - 4
server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/reservedstate/ReservedRepositoryAction.java

@@ -34,7 +34,7 @@ import static org.elasticsearch.common.xcontent.XContentHelper.mapToXContentPars
  * It is used by the ReservedClusterStateService to add/update or remove snapshot repositories. Typical usage
  * for this action is in the context of file based settings.
  */
-public class ReservedRepositoryAction implements ReservedClusterStateHandler<ClusterState, List<PutRepositoryRequest>> {
+public class ReservedRepositoryAction implements ReservedClusterStateHandler<List<PutRepositoryRequest>> {
     public static final String NAME = "snapshot_repositories";
 
     private final RepositoriesService repositoriesService;
@@ -67,8 +67,7 @@ public class ReservedRepositoryAction implements ReservedClusterStateHandler<Clu
     }
 
     @Override
-    public TransformState<ClusterState> transform(List<PutRepositoryRequest> source, TransformState<ClusterState> prevState)
-        throws Exception {
+    public TransformState transform(List<PutRepositoryRequest> source, TransformState prevState) throws Exception {
         var requests = prepare(source);
 
         ClusterState state = prevState.state();
@@ -88,7 +87,7 @@ public class ReservedRepositoryAction implements ReservedClusterStateHandler<Clu
             state = task.execute(state);
         }
 
-        return new TransformState<>(state, entities);
+        return new TransformState(state, entities);
 
     }
 

+ 11 - 5
server/src/main/java/org/elasticsearch/action/admin/indices/template/reservedstate/ReservedComposableIndexTemplateAction.java

@@ -12,13 +12,16 @@ package org.elasticsearch.action.admin.indices.template.reservedstate;
 import org.elasticsearch.action.admin.indices.template.put.PutComponentTemplateAction;
 import org.elasticsearch.action.admin.indices.template.put.TransportPutComponentTemplateAction;
 import org.elasticsearch.action.admin.indices.template.put.TransportPutComposableIndexTemplateAction;
+import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.ComponentTemplate;
 import org.elasticsearch.cluster.metadata.ComposableIndexTemplate;
 import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
+import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.common.settings.IndexScopedSettings;
 import org.elasticsearch.common.util.set.Sets;
 import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
 import org.elasticsearch.reservedstate.TransformState;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xcontent.XContentParserConfiguration;
@@ -45,7 +48,7 @@ import static org.elasticsearch.common.xcontent.XContentHelper.mapToXContentPars
  */
 public class ReservedComposableIndexTemplateAction
     implements
-        ReservedClusterStateHandler<ProjectMetadata, ReservedComposableIndexTemplateAction.ComponentsAndComposables> {
+        ReservedProjectStateHandler<ReservedComposableIndexTemplateAction.ComponentsAndComposables> {
     public static final String NAME = "index_templates";
     public static final String COMPONENTS = "component_templates";
     private static final String COMPONENT_PREFIX = "component_template:";
@@ -133,10 +136,10 @@ public class ReservedComposableIndexTemplateAction
     }
 
     @Override
-    public TransformState<ProjectMetadata> transform(ComponentsAndComposables source, TransformState<ProjectMetadata> prevState)
-        throws Exception {
+    public TransformState transform(ProjectId projectId, ComponentsAndComposables source, TransformState prevState) throws Exception {
         var requests = prepare(source);
-        ProjectMetadata project = prevState.state();
+        ClusterState clusterState = prevState.state();
+        ProjectMetadata project = clusterState.getMetadata().getProject(projectId);
 
         // We transform in the following order:
         // 1. create or update component templates (composable templates depend on them)
@@ -192,7 +195,10 @@ public class ReservedComposableIndexTemplateAction
             project = MetadataIndexTemplateService.innerRemoveComponentTemplate(project, componentNames);
         }
 
-        return new TransformState<>(project, Sets.union(componentEntities, composableEntities));
+        return new TransformState(
+            ClusterState.builder(clusterState).putProjectMetadata(project).build(),
+            Sets.union(componentEntities, composableEntities)
+        );
     }
 
     @Override

+ 13 - 11
server/src/main/java/org/elasticsearch/action/ingest/ReservedPipelineAction.java

@@ -10,11 +10,13 @@
 package org.elasticsearch.action.ingest;
 
 import org.elasticsearch.ElasticsearchGenerationException;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.ingest.IngestMetadata;
 import org.elasticsearch.ingest.IngestService;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
 import org.elasticsearch.reservedstate.TransformState;
 import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentFactory;
@@ -36,7 +38,7 @@ import java.util.stream.Collectors;
  * It is used by the ReservedClusterStateService to add/update or remove ingest pipelines. Typical usage
  * for this action is in the context of file based state.
  */
-public class ReservedPipelineAction implements ReservedClusterStateHandler<ProjectMetadata, List<PutPipelineRequest>> {
+public class ReservedPipelineAction implements ReservedProjectStateHandler<List<PutPipelineRequest>> {
     public static final String NAME = "ingest_pipelines";
 
     /**
@@ -78,21 +80,21 @@ public class ReservedPipelineAction implements ReservedClusterStateHandler<Proje
     }
 
     @Override
-    public TransformState<ProjectMetadata> transform(List<PutPipelineRequest> source, TransformState<ProjectMetadata> prevState)
-        throws Exception {
+    public TransformState transform(ProjectId projectId, List<PutPipelineRequest> source, TransformState prevState) throws Exception {
         var requests = prepare(source);
 
-        ProjectMetadata state = prevState.state();
+        ClusterState clusterState = prevState.state();
+        ProjectMetadata projectMetadata = clusterState.metadata().getProject(projectId);
 
         for (var request : requests) {
-            var nopUpdate = IngestService.isNoOpPipelineUpdate(state, request);
+            var nopUpdate = IngestService.isNoOpPipelineUpdate(projectMetadata, request);
 
             if (nopUpdate) {
                 continue;
             }
 
-            var task = new IngestService.PutPipelineClusterStateUpdateTask(state.id(), request);
-            state = wrapIngestTaskExecute(task, state);
+            var task = new IngestService.PutPipelineClusterStateUpdateTask(projectMetadata.id(), request);
+            projectMetadata = wrapIngestTaskExecute(task, projectMetadata);
         }
 
         Set<String> entities = requests.stream().map(PutPipelineRequest::getId).collect(Collectors.toSet());
@@ -102,7 +104,7 @@ public class ReservedPipelineAction implements ReservedClusterStateHandler<Proje
 
         for (var pipelineToDelete : toDelete) {
             var task = new IngestService.DeletePipelineClusterStateUpdateTask(
-                state.id(),
+                projectMetadata.id(),
                 null,
                 new DeletePipelineRequest(
                     RESERVED_CLUSTER_STATE_HANDLER_IGNORED_TIMEOUT,
@@ -110,10 +112,10 @@ public class ReservedPipelineAction implements ReservedClusterStateHandler<Proje
                     pipelineToDelete
                 )
             );
-            state = wrapIngestTaskExecute(task, state);
+            projectMetadata = wrapIngestTaskExecute(task, projectMetadata);
         }
 
-        return new TransformState<>(state, entities);
+        return new TransformState(ClusterState.builder(clusterState).putProjectMetadata(projectMetadata).build(), entities);
     }
 
     @Override

+ 9 - 9
server/src/main/java/org/elasticsearch/node/NodeConstruction.java

@@ -50,7 +50,6 @@ import org.elasticsearch.cluster.metadata.MetadataCreateIndexService;
 import org.elasticsearch.cluster.metadata.MetadataDataStreamsService;
 import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
 import org.elasticsearch.cluster.metadata.MetadataUpdateSettingsService;
-import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.cluster.metadata.SystemIndexMetadataUpgradeService;
 import org.elasticsearch.cluster.metadata.TemplateUpgradeService;
 import org.elasticsearch.cluster.node.DiscoveryNode;
@@ -189,7 +188,8 @@ import org.elasticsearch.readiness.ReadinessService;
 import org.elasticsearch.repositories.RepositoriesModule;
 import org.elasticsearch.repositories.RepositoriesService;
 import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
 import org.elasticsearch.reservedstate.action.ReservedClusterSettingsAction;
 import org.elasticsearch.reservedstate.service.FileSettingsService;
 import org.elasticsearch.reservedstate.service.FileSettingsService.FileSettingsHealthIndicatorService;
@@ -995,7 +995,7 @@ class NodeConstruction {
         final ResponseCollectorService responseCollectorService = new ResponseCollectorService(clusterService);
         modules.bindToInstance(ResponseCollectorService.class, responseCollectorService);
 
-        var reservedStateHandlerProviders = pluginsService.loadServiceProviders(ReservedClusterStateHandlerProvider.class);
+        var reservedStateHandlerProviders = pluginsService.loadServiceProviders(ReservedStateHandlerProvider.class);
 
         ActionModule actionModule = new ActionModule(
             settings,
@@ -1599,11 +1599,11 @@ class NodeConstruction {
         return b -> b.bind(PersistedClusterStateService.class).toInstance(service);
     }
 
-    private List<ReservedClusterStateHandler<ClusterState, ?>> buildReservedClusterStateHandlers(
-        List<? extends ReservedClusterStateHandlerProvider> handlers,
+    private List<ReservedClusterStateHandler<?>> buildReservedClusterStateHandlers(
+        List<? extends ReservedStateHandlerProvider> handlers,
         SettingsModule settingsModule
     ) {
-        List<ReservedClusterStateHandler<ClusterState, ?>> reservedStateHandlers = new ArrayList<>();
+        List<ReservedClusterStateHandler<?>> reservedStateHandlers = new ArrayList<>();
 
         // add all reserved state handlers from server
         reservedStateHandlers.add(new ReservedClusterSettingsAction(settingsModule.getClusterSettings()));
@@ -1614,8 +1614,8 @@ class NodeConstruction {
         return reservedStateHandlers;
     }
 
-    private List<ReservedClusterStateHandler<ProjectMetadata, ?>> buildReservedProjectStateHandlers(
-        List<? extends ReservedClusterStateHandlerProvider> handlers,
+    private List<ReservedProjectStateHandler<?>> buildReservedProjectStateHandlers(
+        List<? extends ReservedStateHandlerProvider> handlers,
         SettingsModule settingsModule,
         ClusterService clusterService,
         IndicesService indicesService,
@@ -1624,7 +1624,7 @@ class NodeConstruction {
         MetadataCreateIndexService metadataCreateIndexService,
         DataStreamGlobalRetentionSettings globalRetentionSettings
     ) {
-        List<ReservedClusterStateHandler<ProjectMetadata, ?>> reservedStateHandlers = new ArrayList<>();
+        List<ReservedProjectStateHandler<?>> reservedStateHandlers = new ArrayList<>();
 
         var templateService = new MetadataIndexTemplateService(
             clusterService,

+ 4 - 101
server/src/main/java/org/elasticsearch/reservedstate/ReservedClusterStateHandler.java

@@ -9,43 +9,12 @@
 
 package org.elasticsearch.reservedstate;
 
-import org.elasticsearch.action.ActionRequestValidationException;
-import org.elasticsearch.action.support.master.MasterNodeRequest;
-import org.elasticsearch.core.TimeValue;
-import org.elasticsearch.xcontent.XContentParser;
-
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Collections;
-
 /**
- * Base interface used for implementing 'operator mode' cluster state updates.
- *
- * <p>
- * Reserving cluster state, for file based settings and modules/plugins, requires
- * that we have a separate update handler interface that is different than the REST handlers. This interface class declares
- * the basic contract for implementing cluster state update handlers that result in a cluster state that is effectively immutable
- * by the REST handlers. The only way the reserved cluster state can be updated is through the 'operator mode' actions, e.g. updating
- * the file settings.
- * </p>
+ * {@link ReservedStateHandler} for updating cluster-wide cluster state.
  *
- * @param <S> The state type to be updated by this handler
  * @param <T> The type used to represent the state update
  */
-public interface ReservedClusterStateHandler<S, T> {
-    /**
-     * Unique identifier for the handler.
-     *
-     * <p>
-     * The handler name is a unique identifier that is matched to a section in a
-     * cluster state update content. The reserved cluster state updates are done as a single
-     * cluster state update and the cluster state is typically supplied as a combined content,
-     * unlike the REST handlers. This name must match a desired content key name in the combined
-     * cluster state update, e.g. "ilm" or "cluster_settings" (for persistent cluster settings update).
-     *
-     * @return a String with the handler name, e.g "ilm".
-     */
-    String name();
+public interface ReservedClusterStateHandler<T> extends ReservedStateHandler<T> {
 
     /**
      * The transformation method implemented by the handler.
@@ -58,77 +27,11 @@ public interface ReservedClusterStateHandler<S, T> {
      * {@link TransformState}, which contains the current cluster state as well as any previous keys
      * set by this handler on prior invocation.
      *
-     * @param source The parsed information specific to this handler from the combined cluster state content
+     * @param source    The parsed information specific to this handler from the combined cluster state content
      * @param prevState The previous cluster state and keys set by this handler (if any)
      * @return The modified state and the current keys set by this handler
      * @throws Exception
      */
-    TransformState<S> transform(T source, TransformState<S> prevState) throws Exception;
-
-    /**
-     * List of dependent handler names for this handler.
-     *
-     * <p>
-     * Sometimes certain parts of the cluster state cannot be created/updated without previously
-     * setting other cluster state components, e.g. composable templates. Since the reserved cluster state handlers
-     * are processed in random order by the ReservedClusterStateService, this method gives an opportunity
-     * to any reserved handler to declare other state handlers it depends on. Given dependencies exist,
-     * the ReservedClusterStateService will order those handlers such that the handlers that are dependent
-     * on are processed first.
-     *
-     * @return a collection of reserved state handler names
-     */
-    default Collection<String> dependencies() {
-        return Collections.emptyList();
-    }
-
-    /**
-     * List of optional dependent handler names for this handler.
-     *
-     * <p>
-     * These are dependent handlers which may or may not exist for this handler to be
-     * processed. If the optional dependency exists, then they are simply ordered to be
-     * merged into the cluster state before this handler.
-     *
-     * @return a collection of optional reserved state handler names
-     */
-    default Collection<String> optionalDependencies() {
-        return Collections.emptyList();
-    }
-
-    /**
-     * Generic validation helper method that throws consistent exception for all handlers.
-     *
-     * <p>
-     * All implementations of {@link ReservedClusterStateHandler} should call the request validate method, by calling this default
-     * implementation. To aid in any special validation logic that may need to be implemented by the reserved cluster state handler
-     * we provide this convenience method.
-     *
-     * @param request the master node request that we base this reserved state handler on
-     */
-    default void validate(MasterNodeRequest<?> request) {
-        ActionRequestValidationException exception = request.validate();
-        if (exception != null) {
-            throw new IllegalStateException("Validation error", exception);
-        }
-    }
+    TransformState transform(T source, TransformState prevState) throws Exception;
 
-    /**
-     * The parse content method which is called during parsing of file based content.
-     *
-     * <p>
-     * The immutable state can be provided as XContent, which means that each handler needs
-     * to implement a method to convert an XContent to an object it can consume later in
-     * transform
-     *
-     * @param parser the XContent parser we are parsing from
-     * @return
-     * @throws IOException
-     */
-    T fromXContent(XContentParser parser) throws IOException;
-
-    /**
-     * Reserved-state handlers create master-node requests but never actually send them to the master node so the timeouts are not relevant.
-     */
-    TimeValue RESERVED_CLUSTER_STATE_HANDLER_IGNORED_TIMEOUT = TimeValue.THIRTY_SECONDS;
 }

+ 40 - 0
server/src/main/java/org/elasticsearch/reservedstate/ReservedProjectStateHandler.java

@@ -0,0 +1,40 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.reservedstate;
+
+import org.elasticsearch.cluster.metadata.ProjectId;
+
+/**
+ * {@link ReservedStateHandler} for updating project-specific cluster state.
+ *
+ * @param <T> The type used to represent the state update
+ */
+public interface ReservedProjectStateHandler<T> extends ReservedStateHandler<T> {
+
+    /**
+     * The transformation method implemented by the handler.
+     *
+     * <p>
+     * The transform method of the handler should apply the necessary changes to
+     * the cluster state as it normally would in a REST handler. One difference is that the
+     * transform method in an reserved state handler must perform all CRUD operations of the cluster
+     * state in one go. For that reason, we supply a wrapper class to the cluster state called
+     * {@link TransformState}, which contains the current cluster state as well as any previous keys
+     * set by this handler on prior invocation.
+     *
+     * @param projectId The project id for the update state content
+     * @param source The parsed information specific to this handler from the combined cluster state content
+     * @param prevState The previous cluster state and keys set by this handler (if any)
+     * @return The modified state and the current keys set by this handler
+     * @throws Exception
+     */
+    TransformState transform(ProjectId projectId, T source, TransformState prevState) throws Exception;
+
+}

+ 116 - 0
server/src/main/java/org/elasticsearch/reservedstate/ReservedStateHandler.java

@@ -0,0 +1,116 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.reservedstate;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.support.master.MasterNodeRequest;
+import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * Base interface used for implementing 'operator mode' cluster state updates.
+ *
+ * <p>
+ * Reserving cluster state, for file based settings and modules/plugins, requires
+ * that we have a separate update handler interface that is different than the REST handlers. This interface class declares
+ * the basic contract for implementing cluster state update handlers that result in a cluster state that is effectively immutable
+ * by the REST handlers. The only way the reserved cluster state can be updated is through the 'operator mode' actions, e.g. updating
+ * the file settings.
+ * </p>
+ *
+ * @param <T> The type used to represent the state update
+ */
+public interface ReservedStateHandler<T> {
+
+    /**
+     * Reserved-state handlers create master-node requests but never actually send them to the master node so the timeouts are not relevant.
+     */
+    TimeValue RESERVED_CLUSTER_STATE_HANDLER_IGNORED_TIMEOUT = TimeValue.THIRTY_SECONDS;
+
+    /**
+     * Unique identifier for the handler.
+     *
+     * <p>
+     * The handler name is a unique identifier that is matched to a section in a
+     * cluster state update content. The reserved cluster state updates are done as a single
+     * cluster state update and the cluster state is typically supplied as a combined content,
+     * unlike the REST handlers. This name must match a desired content key name in the combined
+     * cluster state update, e.g. "ilm" or "cluster_settings" (for persistent cluster settings update).
+     *
+     * @return a String with the handler name, e.g "ilm".
+     */
+    String name();
+
+    /**
+     * List of dependent handler names for this handler.
+     *
+     * <p>
+     * Sometimes certain parts of the cluster state cannot be created/updated without previously
+     * setting other cluster state components, e.g. composable templates. Since the reserved cluster state handlers
+     * are processed in random order by the ReservedClusterStateService, this method gives an opportunity
+     * to any reserved handler to declare other state handlers it depends on. Given dependencies exist,
+     * the ReservedClusterStateService will order those handlers such that the handlers that are dependent
+     * on are processed first.
+     *
+     * @return a collection of reserved state handler names
+     */
+    default Collection<String> dependencies() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * List of optional dependent handler names for this handler.
+     *
+     * <p>
+     * These are dependent handlers which may or may not exist for this handler to be
+     * processed. If the optional dependency exists, then they are simply ordered to be
+     * merged into the cluster state before this handler.
+     *
+     * @return a collection of optional reserved state handler names
+     */
+    default Collection<String> optionalDependencies() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Generic validation helper method that throws consistent exception for all handlers.
+     *
+     * <p>
+     * All implementations of {@link ReservedProjectStateHandler} should call the request validate method, by calling this default
+     * implementation. To aid in any special validation logic that may need to be implemented by the reserved cluster state handler
+     * we provide this convenience method.
+     *
+     * @param request the master node request that we base this reserved state handler on
+     */
+    default void validate(MasterNodeRequest<?> request) {
+        ActionRequestValidationException exception = request.validate();
+        if (exception != null) {
+            throw new IllegalStateException("Validation error", exception);
+        }
+    }
+
+    /**
+     * The parse content method which is called during parsing of file based content.
+     *
+     * <p>
+     * The immutable state can be provided as XContent, which means that each handler needs
+     * to implement a method to convert an XContent to an object it can consume later in
+     * transform
+     *
+     * @param parser the XContent parser we are parsing from
+     * @return
+     * @throws IOException
+     */
+    T fromXContent(XContentParser parser) throws IOException;
+}

+ 3 - 6
server/src/main/java/org/elasticsearch/reservedstate/ReservedClusterStateHandlerProvider.java → server/src/main/java/org/elasticsearch/reservedstate/ReservedStateHandlerProvider.java

@@ -9,9 +9,6 @@
 
 package org.elasticsearch.reservedstate;
 
-import org.elasticsearch.cluster.ClusterState;
-import org.elasticsearch.cluster.metadata.ProjectMetadata;
-
 import java.util.Collection;
 import java.util.Set;
 
@@ -19,18 +16,18 @@ import java.util.Set;
  * SPI service interface for supplying {@link ReservedClusterStateHandler} implementations to Elasticsearch
  * from plugins/modules.
  */
-public interface ReservedClusterStateHandlerProvider {
+public interface ReservedStateHandlerProvider {
     /**
      * Returns a list of {@link ReservedClusterStateHandler} implementations for updating cluster state.
      */
-    default Collection<ReservedClusterStateHandler<ClusterState, ?>> clusterHandlers() {
+    default Collection<ReservedClusterStateHandler<?>> clusterHandlers() {
         return Set.of();
     }
 
     /**
      * Returns a list of {@link ReservedClusterStateHandler} implementations for updating project state.
      */
-    default Collection<ReservedClusterStateHandler<ProjectMetadata, ?>> projectHandlers() {
+    default Collection<ReservedProjectStateHandler<?>> projectHandlers() {
         return Set.of();
     }
 }

+ 5 - 5
server/src/main/java/org/elasticsearch/reservedstate/TransformState.java

@@ -9,13 +9,13 @@
 
 package org.elasticsearch.reservedstate;
 
+import org.elasticsearch.cluster.ClusterState;
+
 import java.util.Set;
 
 /**
- * A state wrapper used by the ReservedClusterStateService to pass the
+ * A {@link org.elasticsearch.cluster.ClusterState} wrapper used by the ReservedClusterStateService to pass the
  * current state as well as previous keys set by an {@link ReservedClusterStateHandler} to each transform
- * step of the state update.
- *
- * @param <S> The type of state to update
+ * step of the cluster state update.
  */
-public record TransformState<S>(S state, Set<String> keys) {}
+public record TransformState(ClusterState state, Set<String> keys) {}

+ 3 - 4
server/src/main/java/org/elasticsearch/reservedstate/action/ReservedClusterSettingsAction.java

@@ -12,7 +12,6 @@ package org.elasticsearch.reservedstate.action;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
-import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.common.settings.ClusterSettings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.settings.SettingsUpdater;
@@ -32,7 +31,7 @@ import java.util.stream.Collectors;
  * It is used by the ReservedClusterStateService to update the persistent cluster settings.
  * Since transient cluster settings are deprecated, this action doesn't support updating transient cluster settings.
  */
-public class ReservedClusterSettingsAction implements ReservedClusterStateHandler<ClusterState, Map<String, Object>> {
+public class ReservedClusterSettingsAction implements ReservedClusterStateHandler<Map<String, Object>> {
 
     private static final Logger logger = LogManager.getLogger(ReservedClusterSettingsAction.class);
 
@@ -69,7 +68,7 @@ public class ReservedClusterSettingsAction implements ReservedClusterStateHandle
     }
 
     @Override
-    public TransformState<ClusterState> transform(Map<String, Object> input, TransformState<ClusterState> prevState) {
+    public TransformState transform(Map<String, Object> input, TransformState prevState) {
         ClusterUpdateSettingsRequest request = prepare(input, prevState.keys());
 
         // allow empty requests, this is how we clean up settings
@@ -90,7 +89,7 @@ public class ReservedClusterSettingsAction implements ReservedClusterStateHandle
             .filter(k -> request.persistentSettings().hasValue(k))
             .collect(Collectors.toSet());
 
-        return new TransformState<>(state, currentKeys);
+        return new TransformState(state, currentKeys);
     }
 
     @Override

+ 6 - 17
server/src/main/java/org/elasticsearch/reservedstate/service/ProjectClusterStateHandlerAdapter.java

@@ -10,22 +10,21 @@
 package org.elasticsearch.reservedstate.service;
 
 import org.elasticsearch.action.support.master.MasterNodeRequest;
-import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.ProjectId;
-import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
 import org.elasticsearch.reservedstate.TransformState;
 import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
 import java.util.Collection;
 
-class ProjectClusterStateHandlerAdapter<T> implements ReservedClusterStateHandler<ClusterState, T> {
+class ProjectClusterStateHandlerAdapter<T> implements ReservedClusterStateHandler<T> {
 
     private final ProjectId projectId;
-    private final ReservedClusterStateHandler<ProjectMetadata, T> handler;
+    private final ReservedProjectStateHandler<T> handler;
 
-    ProjectClusterStateHandlerAdapter(ProjectId projectId, ReservedClusterStateHandler<ProjectMetadata, T> handler) {
+    ProjectClusterStateHandlerAdapter(ProjectId projectId, ReservedProjectStateHandler<T> handler) {
         this.projectId = projectId;
         this.handler = handler;
     }
@@ -56,18 +55,8 @@ class ProjectClusterStateHandlerAdapter<T> implements ReservedClusterStateHandle
     }
 
     @Override
-    public TransformState<ClusterState> transform(T source, TransformState<ClusterState> prevState) throws Exception {
-        ProjectMetadata project = prevState.state().metadata().getProject(projectId);
-
-        TransformState<ProjectMetadata> oldProjectState = new TransformState<>(project, prevState.keys());
-        TransformState<ProjectMetadata> newProjectState = handler.transform(source, oldProjectState);
-
-        return newProjectState == oldProjectState
-            ? prevState
-            : new TransformState<>(
-                ClusterState.builder(prevState.state()).putProjectMetadata(newProjectState.state()).build(),
-                newProjectState.keys()
-            );
+    public TransformState transform(T source, TransformState prevState) throws Exception {
+        return handler.transform(projectId, source, prevState);
     }
 
     @Override

+ 77 - 27
server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java

@@ -27,6 +27,8 @@ import org.elasticsearch.core.FixForMultiProject;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.env.BuildVersion;
 import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
+import org.elasticsearch.reservedstate.ReservedStateHandler;
 import org.elasticsearch.reservedstate.TransformState;
 import org.elasticsearch.xcontent.ConstructingObjectParser;
 import org.elasticsearch.xcontent.ParseField;
@@ -66,9 +68,9 @@ public class ReservedClusterStateService {
     public static final ParseField STATE_FIELD = new ParseField("state");
     public static final ParseField METADATA_FIELD = new ParseField("metadata");
 
-    private final Map<String, ReservedClusterStateHandler<?, ?>> allHandlers;
-    private final Map<String, ReservedClusterStateHandler<ClusterState, ?>> clusterHandlers;
-    private final Map<String, ReservedClusterStateHandler<ProjectMetadata, ?>> projectHandlers;
+    private final Map<String, ReservedStateHandler<?>> allHandlers;
+    private final Map<String, ReservedClusterStateHandler<?>> clusterHandlers;
+    private final Map<String, ReservedProjectStateHandler<?>> projectHandlers;
     private final ClusterService clusterService;
     private final ReservedStateUpdateTaskExecutor updateTaskExecutor;
     private final ReservedStateErrorTaskExecutor errorTaskExecutor;
@@ -87,9 +89,7 @@ public class ReservedClusterStateService {
         }
     );
 
-    private static <T> ReservedClusterStateHandler<ClusterState, T> adaptForDefaultProject(
-        ReservedClusterStateHandler<ProjectMetadata, T> handler
-    ) {
+    private static <T> ReservedClusterStateHandler<T> adaptForDefaultProject(ReservedProjectStateHandler<T> handler) {
         return new ProjectClusterStateHandlerAdapter<>(Metadata.DEFAULT_PROJECT_ID, handler);
     }
 
@@ -107,15 +107,15 @@ public class ReservedClusterStateService {
     public ReservedClusterStateService(
         ClusterService clusterService,
         RerouteService rerouteService,
-        List<ReservedClusterStateHandler<ClusterState, ?>> clusterHandlerList,
-        List<ReservedClusterStateHandler<ProjectMetadata, ?>> projectHandlerList
+        List<ReservedClusterStateHandler<?>> clusterHandlerList,
+        List<ReservedProjectStateHandler<?>> projectHandlerList
     ) {
         this.clusterService = clusterService;
         this.updateTaskExecutor = new ReservedStateUpdateTaskExecutor(rerouteService);
         this.errorTaskExecutor = new ReservedStateErrorTaskExecutor();
 
         allHandlers = Stream.concat(clusterHandlerList.stream(), projectHandlerList.stream())
-            .collect(Collectors.toMap(ReservedClusterStateHandler::name, Function.identity(), (v1, v2) -> {
+            .collect(Collectors.toMap(ReservedStateHandler::name, Function.identity(), (v1, v2) -> {
                 throw new IllegalArgumentException("Duplicate handler name: [" + v1.name() + "]");
             }));
         // project handlers also need to be cluster state handlers for the default project,
@@ -124,10 +124,10 @@ public class ReservedClusterStateService {
             clusterHandlerList.stream(),
             projectHandlerList.stream().map(ReservedClusterStateService::adaptForDefaultProject)
         ).collect(Collectors.toMap(ReservedClusterStateHandler::name, Function.identity()));
-        projectHandlers = projectHandlerList.stream().collect(Collectors.toMap(ReservedClusterStateHandler::name, Function.identity()));
+        projectHandlers = projectHandlerList.stream().collect(Collectors.toMap(ReservedStateHandler::name, Function.identity()));
 
         stateChunkParser.declareNamedObjects(ConstructingObjectParser.constructorArg(), (p, c, name) -> {
-            ReservedClusterStateHandler<?, ?> handler = allHandlers.get(name);
+            ReservedStateHandler<?> handler = allHandlers.get(name);
             if (handler == null) {
                 throw new IllegalStateException("Missing handler definition for content key [" + name + "]");
             }
@@ -447,7 +447,10 @@ public class ReservedClusterStateService {
         }
 
         ClusterState state = clusterService.state();
+        // use an empty project if it doesn't exist, this is then added to ClusterState below.
         ProjectMetadata projectMetadata = getPotentiallyNewProject(state, projectId);
+        state = ClusterState.builder(state).putProjectMetadata(projectMetadata).build();
+
         ReservedStateMetadata existingMetadata = projectMetadata.reservedStateMetadata().get(namespace);
 
         // We check if we should exit early on the state version from clusterService. The ReservedStateUpdateTask
@@ -458,7 +461,7 @@ public class ReservedClusterStateService {
         }
 
         // We trial run all handler validations to ensure that we can process all of the cluster state error free.
-        var trialRunErrors = trialRun(namespace, projectMetadata, reservedStateChunk, orderedHandlers);
+        var trialRunErrors = trialRun(namespace, state, reservedStateChunk, orderedHandlers);
         // this is not using the modified trial state above, but that doesn't matter, we're just setting errors here
         var error = checkAndReportError(Optional.of(projectId), namespace, trialRunErrors, reservedStateVersion, versionCheck);
 
@@ -635,37 +638,74 @@ public class ReservedClusterStateService {
      * @return Any errors that occurred
      */
     List<String> trialRun(
+        ProjectId projectId,
         String namespace,
-        ProjectMetadata currentState,
+        ClusterState currentState,
         ReservedStateChunk stateChunk,
         SequencedSet<String> orderedHandlers
     ) {
-        return trialRun(currentState.reservedStateMetadata().get(namespace), currentState, stateChunk, projectHandlers, orderedHandlers);
+        return trialRun(
+            projectId,
+            currentState.metadata().reservedStateMetadata().get(namespace),
+            currentState,
+            stateChunk,
+            projectHandlers,
+            orderedHandlers
+        );
     }
 
-    private static <S> List<String> trialRun(
+    private static List<String> trialRun(
+        ProjectId projectId,
         ReservedStateMetadata existingMetadata,
-        S currentMetadata,
+        ClusterState currentState,
         ReservedStateChunk stateChunk,
-        Map<String, ReservedClusterStateHandler<S, ?>> handlers,
+        Map<String, ReservedProjectStateHandler<?>> handlers,
         SequencedSet<String> orderedHandlers
     ) {
         Map<String, Object> reservedState = stateChunk.state();
 
         List<String> errors = new ArrayList<>();
 
-        S metadata = currentMetadata;
+        for (var handlerName : orderedHandlers) {
+            ReservedProjectStateHandler<?> handler = handlers.get(handlerName);
+            try {
+                Set<String> existingKeys = keysForHandler(existingMetadata, handlerName);
+                TransformState transformState = transform(
+                    handler,
+                    projectId,
+                    reservedState.get(handlerName),
+                    new TransformState(currentState, existingKeys)
+                );
+                currentState = transformState.state();
+            } catch (Exception e) {
+                errors.add(format("Error processing %s state change: %s", handler.name(), stackTrace(e)));
+            }
+        }
+
+        return errors;
+    }
+
+    private static List<String> trialRun(
+        ReservedStateMetadata existingMetadata,
+        ClusterState currentState,
+        ReservedStateChunk stateChunk,
+        Map<String, ReservedClusterStateHandler<?>> handlers,
+        SequencedSet<String> orderedHandlers
+    ) {
+        Map<String, Object> reservedState = stateChunk.state();
+
+        List<String> errors = new ArrayList<>();
 
         for (var handlerName : orderedHandlers) {
-            ReservedClusterStateHandler<S, ?> handler = handlers.get(handlerName);
+            ReservedClusterStateHandler<?> handler = handlers.get(handlerName);
             try {
                 Set<String> existingKeys = keysForHandler(existingMetadata, handlerName);
-                TransformState<S> transformState = transform(
+                TransformState transformState = transform(
                     handler,
                     reservedState.get(handlerName),
-                    new TransformState<>(metadata, existingKeys)
+                    new TransformState(currentState, existingKeys)
                 );
-                metadata = transformState.state();
+                currentState = transformState.state();
             } catch (Exception e) {
                 errors.add(format("Error processing %s state change: %s", handler.name(), stackTrace(e)));
             }
@@ -675,11 +715,21 @@ public class ReservedClusterStateService {
     }
 
     @SuppressWarnings("unchecked")
-    static <S, T> TransformState<S> transform(ReservedClusterStateHandler<S, T> handler, Object state, TransformState<S> transform)
+    static <S, T> TransformState transform(ReservedClusterStateHandler<T> handler, Object state, TransformState transform)
         throws Exception {
         return handler.transform((T) state, transform);
     }
 
+    @SuppressWarnings("unchecked")
+    static <S, T> TransformState transform(
+        ReservedProjectStateHandler<T> handler,
+        ProjectId projectId,
+        Object state,
+        TransformState transformState
+    ) throws Exception {
+        return handler.transform(projectId, (T) state, transformState);
+    }
+
     /**
      * Returns an ordered set ({@link LinkedHashSet}) of the cluster state handlers that need to
      * execute for a given list of handler names supplied through the {@link ReservedStateChunk}.
@@ -713,7 +763,7 @@ public class ReservedClusterStateService {
     }
 
     private void addStateHandler(
-        Map<String, ? extends ReservedClusterStateHandler<?, ?>> handlers,
+        Map<String, ? extends ReservedStateHandler<?>> handlers,
         String key,
         Set<String> keys,
         SequencedSet<String> ordered,
@@ -735,7 +785,7 @@ public class ReservedClusterStateService {
         }
 
         visited.add(key);
-        ReservedClusterStateHandler<?, ?> handler = handlers.get(key);
+        ReservedStateHandler<?> handler = handlers.get(key);
 
         if (handler == null) {
             throw new IllegalStateException("Unknown handler type: " + key);
@@ -762,7 +812,7 @@ public class ReservedClusterStateService {
      * Adds additional {@link ReservedClusterStateHandler} to the handler registry
      * @param handler an additional reserved state handler to be added
      */
-    public void installClusterStateHandler(ReservedClusterStateHandler<ClusterState, ?> handler) {
+    public void installClusterStateHandler(ReservedClusterStateHandler<?> handler) {
         allHandlers.put(handler.name(), handler);
         clusterHandlers.put(handler.name(), handler);
     }
@@ -771,7 +821,7 @@ public class ReservedClusterStateService {
      * Adds additional {@link ReservedClusterStateHandler} to the handler registry
      * @param handler an additional reserved state handler to be added
      */
-    public void installProjectStateHandler(ReservedClusterStateHandler<ProjectMetadata, ?> handler) {
+    public void installProjectStateHandler(ReservedProjectStateHandler<?> handler) {
         allHandlers.put(handler.name(), handler);
         projectHandlers.put(handler.name(), handler);
         clusterHandlers.put(handler.name(), adaptForDefaultProject(handler));

+ 11 - 4
server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateUpdateTask.java

@@ -16,18 +16,19 @@ import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.gateway.GatewayService;
 import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.TransformState;
 
 import java.util.Collection;
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.Consumer;
 
-public class ReservedClusterStateUpdateTask extends ReservedStateUpdateTask<ClusterState> {
+public class ReservedClusterStateUpdateTask extends ReservedStateUpdateTask<ReservedClusterStateHandler<?>> {
     public ReservedClusterStateUpdateTask(
         String namespace,
         ReservedStateChunk stateChunk,
         ReservedStateVersionCheck versionCheck,
-        Map<String, ReservedClusterStateHandler<ClusterState, ?>> handlers,
+        Map<String, ReservedClusterStateHandler<?>> handlers,
         Collection<String> orderedHandlers,
         Consumer<ErrorState> errorReporter,
         ActionListener<ActionResponse.Empty> listener
@@ -41,7 +42,13 @@ public class ReservedClusterStateUpdateTask extends ReservedStateUpdateTask<Clus
     }
 
     @Override
-    protected ClusterState execute(ClusterState currentState) {
+    protected TransformState transform(ReservedClusterStateHandler<?> handler, Object state, TransformState transformState)
+        throws Exception {
+        return ReservedClusterStateService.transform(handler, state, transformState);
+    }
+
+    @Override
+    ClusterState execute(ClusterState currentState) {
         if (currentState.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)) {
             // If cluster state has become blocked, this task was submitted while the node was master but is now not master.
             // The new master will re-read file settings, so whatever update was to be written here will be handled
@@ -49,7 +56,7 @@ public class ReservedClusterStateUpdateTask extends ReservedStateUpdateTask<Clus
             return currentState;
         }
 
-        var result = execute(currentState, currentState.metadata().reservedStateMetadata());
+        var result = execute(currentState, currentState.getMetadata().reservedStateMetadata());
         if (result == null) {
             return currentState;
         }

+ 17 - 7
server/src/main/java/org/elasticsearch/reservedstate/service/ReservedProjectStateUpdateTask.java

@@ -15,14 +15,15 @@ import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.gateway.GatewayService;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
+import org.elasticsearch.reservedstate.TransformState;
 
 import java.util.Collection;
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.Consumer;
 
-public class ReservedProjectStateUpdateTask extends ReservedStateUpdateTask<ProjectMetadata> {
+public class ReservedProjectStateUpdateTask extends ReservedStateUpdateTask<ReservedProjectStateHandler<?>> {
     private final ProjectId projectId;
 
     public ReservedProjectStateUpdateTask(
@@ -30,7 +31,7 @@ public class ReservedProjectStateUpdateTask extends ReservedStateUpdateTask<Proj
         String namespace,
         ReservedStateChunk stateChunk,
         ReservedStateVersionCheck versionCheck,
-        Map<String, ReservedClusterStateHandler<ProjectMetadata, ?>> handlers,
+        Map<String, ReservedProjectStateHandler<?>> handlers,
         Collection<String> orderedHandlers,
         Consumer<ErrorState> errorReporter,
         ActionListener<ActionResponse.Empty> listener
@@ -44,6 +45,12 @@ public class ReservedProjectStateUpdateTask extends ReservedStateUpdateTask<Proj
         return Optional.of(projectId);
     }
 
+    @Override
+    protected TransformState transform(ReservedProjectStateHandler<?> handler, Object state, TransformState transformState)
+        throws Exception {
+        return ReservedClusterStateService.transform(handler, projectId, state, transformState);
+    }
+
     @Override
     protected ClusterState execute(ClusterState currentState) {
         if (currentState.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)) {
@@ -54,13 +61,16 @@ public class ReservedProjectStateUpdateTask extends ReservedStateUpdateTask<Proj
         }
 
         // use an empty project if it doesnt exist, this is then added to ClusterState below.
-        ProjectMetadata project = ReservedClusterStateService.getPotentiallyNewProject(currentState, projectId);
-
-        var result = execute(project, project.reservedStateMetadata());
+        ProjectMetadata currentProject = ReservedClusterStateService.getPotentiallyNewProject(currentState, projectId);
+        var result = execute(
+            ClusterState.builder(currentState).putProjectMetadata(currentProject).build(),
+            currentProject.reservedStateMetadata()
+        );
         if (result == null) {
             return currentState;
         }
 
-        return ClusterState.builder(currentState).putProjectMetadata(ProjectMetadata.builder(result.v1()).put(result.v2())).build();
+        ProjectMetadata updatedProject = result.v1().getMetadata().getProject(projectId);
+        return ClusterState.builder(currentState).putProjectMetadata(ProjectMetadata.builder(updatedProject).put(result.v2())).build();
     }
 }

+ 10 - 12
server/src/main/java/org/elasticsearch/reservedstate/service/ReservedStateUpdateTask.java

@@ -20,7 +20,7 @@ import org.elasticsearch.cluster.metadata.ReservedStateErrorMetadata;
 import org.elasticsearch.cluster.metadata.ReservedStateHandlerMetadata;
 import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
 import org.elasticsearch.core.Tuple;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedStateHandler;
 import org.elasticsearch.reservedstate.TransformState;
 
 import java.util.ArrayList;
@@ -42,13 +42,13 @@ import static org.elasticsearch.core.Strings.format;
  * Reserved cluster state can only be modified by using the {@link ReservedClusterStateService}. Updating
  * the reserved cluster state through REST APIs is not permitted.
  */
-public abstract class ReservedStateUpdateTask<S> implements ClusterStateTaskListener {
+public abstract class ReservedStateUpdateTask<T extends ReservedStateHandler<?>> implements ClusterStateTaskListener {
     private static final Logger logger = LogManager.getLogger(ReservedStateUpdateTask.class);
 
     private final String namespace;
     private final ReservedStateChunk stateChunk;
     private final ReservedStateVersionCheck versionCheck;
-    private final Map<String, ReservedClusterStateHandler<S, ?>> handlers;
+    private final Map<String, T> handlers;
     private final Collection<String> orderedHandlers;
     private final Consumer<ErrorState> errorReporter;
     private final ActionListener<ActionResponse.Empty> listener;
@@ -57,7 +57,7 @@ public abstract class ReservedStateUpdateTask<S> implements ClusterStateTaskList
         String namespace,
         ReservedStateChunk stateChunk,
         ReservedStateVersionCheck versionCheck,
-        Map<String, ReservedClusterStateHandler<S, ?>> handlers,
+        Map<String, T> handlers,
         Collection<String> orderedHandlers,
         Consumer<ErrorState> errorReporter,
         ActionListener<ActionResponse.Empty> listener
@@ -82,13 +82,15 @@ public abstract class ReservedStateUpdateTask<S> implements ClusterStateTaskList
 
     protected abstract Optional<ProjectId> projectId();
 
-    protected abstract ClusterState execute(ClusterState state);
+    protected abstract TransformState transform(T handler, Object state, TransformState transformState) throws Exception;
+
+    abstract ClusterState execute(ClusterState currentState);
 
     /**
      * Produces a new state {@code S} with the reserved state info in {@code reservedStateMap}
      * @return A tuple of the new state and new reserved state metadata, or {@code null} if no changes are required.
      */
-    final Tuple<S, ReservedStateMetadata> execute(S state, Map<String, ReservedStateMetadata> reservedStateMap) {
+    final Tuple<ClusterState, ReservedStateMetadata> execute(ClusterState state, Map<String, ReservedStateMetadata> reservedStateMap) {
         Map<String, Object> reservedState = stateChunk.state();
         ReservedStateVersion reservedStateVersion = stateChunk.metadata();
         ReservedStateMetadata reservedStateMetadata = reservedStateMap.get(namespace);
@@ -102,14 +104,10 @@ public abstract class ReservedStateUpdateTask<S> implements ClusterStateTaskList
 
         // Transform the cluster state first
         for (var handlerName : orderedHandlers) {
-            ReservedClusterStateHandler<S, ?> handler = handlers.get(handlerName);
+            T handler = handlers.get(handlerName);
             try {
                 Set<String> existingKeys = keysForHandler(reservedStateMetadata, handlerName);
-                TransformState<S> transformState = ReservedClusterStateService.transform(
-                    handler,
-                    reservedState.get(handlerName),
-                    new TransformState<>(state, existingKeys)
-                );
+                TransformState transformState = transform(handler, reservedState.get(handlerName), new TransformState(state, existingKeys));
                 state = transformState.state();
                 reservedMetadataBuilder.putHandler(new ReservedStateHandlerMetadata(handlerName, transformState.keys()));
             } catch (Exception e) {

+ 5 - 6
server/src/test/java/org/elasticsearch/action/admin/cluster/repositories/reservedstate/ReservedRepositoryActionTests.java

@@ -43,8 +43,7 @@ import static org.mockito.Mockito.spy;
  */
 public class ReservedRepositoryActionTests extends ESTestCase {
 
-    private TransformState<ClusterState> processJSON(ReservedRepositoryAction action, TransformState<ClusterState> prevState, String json)
-        throws Exception {
+    private TransformState processJSON(ReservedRepositoryAction action, TransformState prevState, String json) throws Exception {
         try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)) {
             return action.transform(action.fromXContent(parser), prevState);
         }
@@ -54,7 +53,7 @@ public class ReservedRepositoryActionTests extends ESTestCase {
         var repositoriesService = mockRepositoriesService();
 
         ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build();
-        TransformState<ClusterState> prevState = new TransformState<>(state, Collections.emptySet());
+        TransformState prevState = new TransformState(state, Collections.emptySet());
         ReservedRepositoryAction action = new ReservedRepositoryAction(repositoriesService);
 
         String badPolicyJSON = """
@@ -77,12 +76,12 @@ public class ReservedRepositoryActionTests extends ESTestCase {
         var repositoriesService = mockRepositoriesService();
 
         ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build();
-        TransformState<ClusterState> prevState = new TransformState<>(state, Collections.emptySet());
+        TransformState prevState = new TransformState(state, Collections.emptySet());
         ReservedRepositoryAction action = new ReservedRepositoryAction(repositoriesService);
 
         String emptyJSON = "";
 
-        TransformState<ClusterState> updatedState = processJSON(action, prevState, emptyJSON);
+        TransformState updatedState = processJSON(action, prevState, emptyJSON);
         assertEquals(0, updatedState.keys().size());
         assertEquals(prevState.state(), updatedState.state());
 
@@ -111,7 +110,7 @@ public class ReservedRepositoryActionTests extends ESTestCase {
         var repositoriesService = mockRepositoriesService();
 
         ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build();
-        TransformState<ClusterState> prevState = new TransformState<>(state, Set.of("repo1"));
+        TransformState prevState = new TransformState(state, Set.of("repo1"));
         ReservedRepositoryAction action = new ReservedRepositoryAction(repositoriesService);
 
         String emptyJSON = "";

+ 30 - 36
server/src/test/java/org/elasticsearch/action/admin/indices/template/reservedstate/ReservedComposableIndexTemplateActionTests.java

@@ -40,7 +40,7 @@ import org.elasticsearch.indices.IndicesService;
 import org.elasticsearch.indices.InvalidIndexTemplateException;
 import org.elasticsearch.indices.SystemIndices;
 import org.elasticsearch.reservedstate.ActionWithReservedState;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
 import org.elasticsearch.reservedstate.TransformState;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.MockUtils;
@@ -109,19 +109,18 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
         );
     }
 
-    private <T> TransformState<T> processJSON(
-        ReservedClusterStateHandler<T, ReservedComposableIndexTemplateAction.ComponentsAndComposables> action,
-        TransformState<T> prevState,
+    private <T> TransformState processJSON(
+        ReservedProjectStateHandler<ReservedComposableIndexTemplateAction.ComponentsAndComposables> action,
+        TransformState prevState,
         String json
     ) throws Exception {
         try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)) {
-            return action.transform(action.fromXContent(parser), prevState);
+            return action.transform(projectId, action.fromXContent(parser), prevState);
         }
     }
 
     public void testComponentValidation() {
-        ProjectMetadata project = ProjectMetadata.builder(randomProjectIdOrDefault()).build();
-        TransformState<ProjectMetadata> prevState = new TransformState<>(project, Collections.emptySet());
+        TransformState prevState = transformState();
         var action = new ReservedComposableIndexTemplateAction(templateService, indexScopedSettings);
 
         String badComponentJSON = """
@@ -148,8 +147,7 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
     }
 
     public void testComposableIndexValidation() {
-        ProjectMetadata project = ProjectMetadata.builder(randomProjectIdOrDefault()).build();
-        TransformState<ProjectMetadata> prevState = new TransformState<>(project, Collections.emptySet());
+        TransformState prevState = transformState();
         var action = new ReservedComposableIndexTemplateAction(templateService, indexScopedSettings);
 
         String badComponentJSON = """
@@ -239,17 +237,13 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
     }
 
     public void testAddRemoveComponentTemplates() throws Exception {
-        TransformState<ProjectMetadata> prevState = new TransformState<>(
-            ProjectMetadata.builder(projectId).build(),
-            Collections.emptySet()
-        );
+        TransformState prevState = transformState();
         var action = new ReservedComposableIndexTemplateAction(templateService, indexScopedSettings);
 
         String emptyJSON = "";
 
-        TransformState<ProjectMetadata> updatedState = processJSON(action, prevState, emptyJSON);
+        TransformState updatedState = processJSON(action, prevState, emptyJSON);
         assertThat(updatedState.keys(), empty());
-        assertEquals(prevState.state(), updatedState.state());
 
         String settingsJSON = """
             {
@@ -316,17 +310,13 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
     }
 
     public void testAddRemoveIndexTemplates() throws Exception {
-        TransformState<ProjectMetadata> prevState = new TransformState<>(
-            ProjectMetadata.builder(projectId).build(),
-            Collections.emptySet()
-        );
+        TransformState prevState = transformState();
         var action = new ReservedComposableIndexTemplateAction(templateService, indexScopedSettings);
 
         String emptyJSON = "";
 
-        TransformState<ProjectMetadata> updatedState = processJSON(action, prevState, emptyJSON);
+        TransformState updatedState = processJSON(action, prevState, emptyJSON);
         assertThat(updatedState.keys(), empty());
-        assertEquals(prevState.state(), updatedState.state());
 
         String settingsJSON = """
             {
@@ -510,17 +500,13 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
     }
 
     public void testAddRemoveIndexTemplatesWithOverlap() throws Exception {
-        TransformState<ProjectMetadata> prevState = new TransformState<>(
-            ProjectMetadata.builder(projectId).build(),
-            Collections.emptySet()
-        );
+        TransformState prevState = transformState();
         var action = new ReservedComposableIndexTemplateAction(templateService, indexScopedSettings);
 
         String emptyJSON = "";
 
-        TransformState<ProjectMetadata> updatedState = processJSON(action, prevState, emptyJSON);
+        TransformState updatedState = processJSON(action, prevState, emptyJSON);
         assertThat(updatedState.keys(), empty());
-        assertEquals(prevState.state(), updatedState.state());
 
         // Adding two composable index templates with same index patterns will fail
         String settingsJSON = """
@@ -746,10 +732,7 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
     }
 
     public void testBlockUsingReservedComponentTemplates() throws Exception {
-        TransformState<ProjectMetadata> prevState = new TransformState<>(
-            ProjectMetadata.builder(projectId).build(),
-            Collections.emptySet()
-        );
+        TransformState prevState = transformState();
         var action = new ReservedComposableIndexTemplateAction(templateService, indexScopedSettings);
 
         String settingsJSON = """
@@ -771,7 +754,7 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
 
         var updatedState = processJSON(action, prevState, settingsJSON);
 
-        ProjectMetadata withReservedState = ProjectMetadata.builder(updatedState.state())
+        ProjectMetadata withReservedState = ProjectMetadata.builder(updatedState.state().getMetadata().getProject(projectId))
             .put(
                 ReservedStateMetadata.builder("test")
                     .putHandler(new ReservedStateHandlerMetadata(ReservedComposableIndexTemplateAction.NAME, updatedState.keys()))
@@ -924,10 +907,10 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
         // we should see the weird composable name prefixed 'validate_template'
         assertThat(project.templatesV2(), allOf(aMapWithSize(1), hasKey(reservedComposableIndexName(conflictingTemplateName))));
 
-        TransformState<ProjectMetadata> prevState = new TransformState<>(project, Collections.emptySet());
+        TransformState prevState = transformState(project);
         var action = new ReservedComposableIndexTemplateAction(mockedTemplateService, indexScopedSettings);
 
-        TransformState<ProjectMetadata> updatedState = processJSON(action, prevState, composableTemplate);
+        TransformState updatedState = processJSON(action, prevState, composableTemplate);
 
         // only one reserved key for 'validate_template'
         assertThat(updatedState.keys(), containsInAnyOrder(reservedComposableIndexName(conflictingTemplateName)));
@@ -935,11 +918,11 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
         // added that weird name 'composable_index_template:validate_template', using this prefix in the name shouldn't make us fail
         // any reservation validation
         assertThat(
-            updatedState.state().templatesV2(),
+            updatedState.state().getMetadata().getProject(projectId).templatesV2(),
             allOf(aMapWithSize(2), hasKey(reservedComposableIndexName(conflictingTemplateName)), hasKey(conflictingTemplateName))
         );
 
-        ProjectMetadata withReservedMetadata = ProjectMetadata.builder(updatedState.state())
+        ProjectMetadata withReservedMetadata = ProjectMetadata.builder(updatedState.state().getMetadata().getProject(projectId))
             .put(
                 new ReservedStateMetadata.Builder("file_settings").putHandler(
                     new ReservedStateHandlerMetadata(ReservedComposableIndexTemplateAction.NAME, updatedState.keys())
@@ -996,4 +979,15 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
             prOK.name()
         );
     }
+
+    private TransformState transformState() {
+        return transformState(ProjectMetadata.builder(projectId).build());
+    }
+
+    private TransformState transformState(ProjectMetadata projectMetadata) {
+        return new TransformState(
+            ClusterState.builder(ClusterName.DEFAULT).putProjectMetadata(projectMetadata).build(),
+            Collections.emptySet()
+        );
+    }
 }

+ 16 - 13
server/src/test/java/org/elasticsearch/action/ingest/ReservedPipelineActionTests.java

@@ -9,6 +9,9 @@
 
 package org.elasticsearch.action.ingest;
 
+import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.reservedstate.TransformState;
 import org.elasticsearch.test.ESTestCase;
@@ -23,26 +26,26 @@ import static org.hamcrest.Matchers.empty;
 
 public class ReservedPipelineActionTests extends ESTestCase {
 
-    private TransformState<ProjectMetadata> processJSON(
-        ReservedPipelineAction action,
-        TransformState<ProjectMetadata> prevState,
-        String json
-    ) throws Exception {
+    private TransformState processJSON(ProjectId projectId, ReservedPipelineAction action, TransformState prevState, String json)
+        throws Exception {
         try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)) {
-            return action.transform(action.fromXContent(parser), prevState);
+            return action.transform(projectId, action.fromXContent(parser), prevState);
         }
     }
 
     public void testAddRemoveIngestPipeline() throws Exception {
-        ProjectMetadata state = ProjectMetadata.builder(randomProjectIdOrDefault()).build();
-        TransformState<ProjectMetadata> prevState = new TransformState<>(state, Collections.emptySet());
+        ProjectId projectId = randomProjectIdOrDefault();
+        ProjectMetadata projectMetadata = ProjectMetadata.builder(projectId).build();
+        TransformState prevState = new TransformState(
+            ClusterState.builder(ClusterName.DEFAULT).putProjectMetadata(projectMetadata).build(),
+            Collections.emptySet()
+        );
         ReservedPipelineAction action = new ReservedPipelineAction();
 
         String emptyJSON = "";
 
-        TransformState<ProjectMetadata> updatedState = processJSON(action, prevState, emptyJSON);
+        TransformState updatedState = processJSON(projectId, action, prevState, emptyJSON);
         assertThat(updatedState.keys(), empty());
-        assertEquals(prevState.state(), updatedState.state());
 
         String json = """
             {
@@ -71,7 +74,7 @@ public class ReservedPipelineActionTests extends ESTestCase {
             }""";
 
         prevState = updatedState;
-        updatedState = processJSON(action, prevState, json);
+        updatedState = processJSON(projectId, action, prevState, json);
         assertThat(updatedState.keys(), containsInAnyOrder("my_ingest_pipeline", "my_ingest_pipeline_1"));
 
         String halfJSON = """
@@ -89,10 +92,10 @@ public class ReservedPipelineActionTests extends ESTestCase {
                }
             }""";
 
-        updatedState = processJSON(action, prevState, halfJSON);
+        updatedState = processJSON(projectId, action, prevState, halfJSON);
         assertThat(updatedState.keys(), containsInAnyOrder("my_ingest_pipeline_1"));
 
-        updatedState = processJSON(action, prevState, emptyJSON);
+        updatedState = processJSON(projectId, action, prevState, emptyJSON);
         assertThat(updatedState.keys(), empty());
     }
 }

+ 2 - 2
server/src/test/java/org/elasticsearch/reservedstate/ReservedClusterStateHandlerTests.java

@@ -21,14 +21,14 @@ import static org.hamcrest.Matchers.is;
 
 public class ReservedClusterStateHandlerTests extends ESTestCase {
     public void testValidation() {
-        ReservedClusterStateHandler<Object, ValidRequest> handler = new ReservedClusterStateHandler<>() {
+        ReservedClusterStateHandler<ValidRequest> handler = new ReservedClusterStateHandler<>() {
             @Override
             public String name() {
                 return "handler";
             }
 
             @Override
-            public TransformState<Object> transform(ValidRequest source, TransformState<Object> prevState) throws Exception {
+            public TransformState transform(ValidRequest source, TransformState prevState) throws Exception {
                 return prevState;
             }
 

+ 7 - 11
server/src/test/java/org/elasticsearch/reservedstate/action/ReservedClusterSettingsActionTests.java

@@ -37,11 +37,7 @@ public class ReservedClusterSettingsActionTests extends ESTestCase {
     static final ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, Set.of(dummySetting1, dummySetting2));
     static final ReservedClusterSettingsAction testAction = new ReservedClusterSettingsAction(clusterSettings);
 
-    private TransformState<ClusterState> processJSON(
-        ReservedClusterSettingsAction action,
-        TransformState<ClusterState> prevState,
-        String json
-    ) throws Exception {
+    private TransformState processJSON(ReservedClusterSettingsAction action, TransformState prevState, String json) throws Exception {
         try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)) {
             return action.transform(parser.map(), prevState);
         }
@@ -51,7 +47,7 @@ public class ReservedClusterSettingsActionTests extends ESTestCase {
         ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
 
         ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build();
-        TransformState<ClusterState> prevState = new TransformState<>(state, Collections.emptySet());
+        TransformState prevState = new TransformState(state, Collections.emptySet());
         ReservedClusterSettingsAction action = new ReservedClusterSettingsAction(clusterSettings);
 
         String badPolicyJSON = """
@@ -69,12 +65,12 @@ public class ReservedClusterSettingsActionTests extends ESTestCase {
         ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
 
         ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build();
-        TransformState<ClusterState> prevState = new TransformState<>(state, Collections.emptySet());
+        TransformState prevState = new TransformState(state, Collections.emptySet());
         ReservedClusterSettingsAction action = new ReservedClusterSettingsAction(clusterSettings);
 
         String emptyJSON = "";
 
-        TransformState<ClusterState> updatedState = processJSON(action, prevState, emptyJSON);
+        TransformState updatedState = processJSON(action, prevState, emptyJSON);
         assertThat(updatedState.keys(), empty());
         assertEquals(prevState.state(), updatedState.state());
 
@@ -123,7 +119,7 @@ public class ReservedClusterSettingsActionTests extends ESTestCase {
             prevSettings,
             logger
         );
-        TransformState<ClusterState> prevState = new TransformState<>(clusterState, Set.of("dummy.setting1"));
+        TransformState prevState = new TransformState(clusterState, Set.of("dummy.setting1"));
 
         String json = """
             {
@@ -134,7 +130,7 @@ public class ReservedClusterSettingsActionTests extends ESTestCase {
             }
             """;
 
-        TransformState<ClusterState> newState = processJSON(testAction, prevState, json);
+        TransformState newState = processJSON(testAction, prevState, json);
         assertThat(newState.keys(), containsInAnyOrder("dummy.setting1", "dummy.setting2"));
         assertThat(newState.state().metadata().persistentSettings().get("dummy.setting1"), is("value1"));
         assertThat(newState.state().metadata().persistentSettings().get("dummy.setting2"), is("value2"));
@@ -146,7 +142,7 @@ public class ReservedClusterSettingsActionTests extends ESTestCase {
                 }
             }
             """;
-        TransformState<ClusterState> newState2 = processJSON(testAction, prevState, jsonRemoval);
+        TransformState newState2 = processJSON(testAction, prevState, jsonRemoval);
         assertThat(newState2.keys(), containsInAnyOrder("dummy.setting2"));
         assertThat(newState2.state().metadata().persistentSettings().get("dummy.setting2"), is("value2"));
     }

+ 52 - 44
server/src/test/java/org/elasticsearch/reservedstate/service/ReservedClusterStateServiceTests.java

@@ -31,6 +31,8 @@ import org.elasticsearch.core.Releasable;
 import org.elasticsearch.env.BuildVersion;
 import org.elasticsearch.env.BuildVersionTests;
 import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
+import org.elasticsearch.reservedstate.ReservedStateHandler;
 import org.elasticsearch.reservedstate.TransformState;
 import org.elasticsearch.reservedstate.action.ReservedClusterSettingsAction;
 import org.elasticsearch.test.ESTestCase;
@@ -134,7 +136,7 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
         }
     }
 
-    private static class TestStateHandler<S> implements ReservedClusterStateHandler<S, Map<String, Object>> {
+    private static class TestStateHandler implements ReservedStateHandler<Map<String, Object>> {
         private final String name;
 
         private TestStateHandler(String name) {
@@ -146,38 +148,33 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
             return name;
         }
 
-        @Override
-        public TransformState<S> transform(Map<String, Object> source, TransformState<S> prevState) throws Exception {
-            return new TransformState<>(prevState.state(), prevState.keys());
-        }
-
         @Override
         public Map<String, Object> fromXContent(XContentParser parser) throws IOException {
             return parser.map();
         }
     }
 
-    private static class TestClusterStateHandler extends TestStateHandler<ClusterState> {
+    private static class TestClusterStateHandler extends TestStateHandler implements ReservedClusterStateHandler<Map<String, Object>> {
         private TestClusterStateHandler(String name) {
             super(name);
         }
 
         @Override
-        public TransformState<ClusterState> transform(Map<String, Object> source, TransformState<ClusterState> prevState) {
+        public TransformState transform(Map<String, Object> source, TransformState prevState) throws Exception {
             ClusterState newState = new ClusterState.Builder(prevState.state()).build();
-            return new TransformState<>(newState, prevState.keys());
+            return new TransformState(newState, prevState.keys());
         }
     }
 
-    private static class TestProjectStateHandler extends TestStateHandler<ProjectMetadata> {
+    private static class TestProjectStateHandler extends TestStateHandler implements ReservedProjectStateHandler<Map<String, Object>> {
         private TestProjectStateHandler(String name) {
             super(name);
         }
 
         @Override
-        public TransformState<ProjectMetadata> transform(Map<String, Object> source, TransformState<ProjectMetadata> prevState) {
-            ProjectMetadata newState = ProjectMetadata.builder(prevState.state()).build();
-            return new TransformState<>(newState, prevState.keys());
+        public TransformState transform(ProjectId projectId, Map<String, Object> source, TransformState prevState) throws Exception {
+            ClusterState newState = new ClusterState.Builder(prevState.state()).build();
+            return new TransformState(newState, prevState.keys());
         }
     }
 
@@ -406,7 +403,7 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
 
         String[] randomStateKeys = generateRandomStringArray(randomIntBetween(5, 10), randomIntBetween(10, 15), false, false);
 
-        List<ReservedClusterStateHandler<ProjectMetadata, ?>> projectHandlers = new ArrayList<>();
+        List<ReservedProjectStateHandler<?>> projectHandlers = new ArrayList<>();
         for (var key : randomStateKeys) {
             projectHandlers.add(spy(new TestProjectStateHandler(key)));
         }
@@ -438,7 +435,7 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
         assertThat(exceptionRef.get(), nullValue());
 
         for (var projectHandler : projectHandlers) {
-            verify(projectHandler, times(1)).transform(any(), any());
+            verify(projectHandler, times(1)).transform(any(), any(), any());
         }
     }
 
@@ -486,8 +483,8 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
 
         assertThat(exceptionRef.get(), notNullValue());
         assertThat(exceptionRef.get().getMessage(), containsString("Failed to merge reserved state chunks because of version mismatch: ["));
-        verify(projectStateHandler1, times(0)).transform(any(), any());
-        verify(projectStateHandler2, times(0)).transform(any(), any());
+        verify(projectStateHandler1, times(0)).transform(any(), any(), any());
+        verify(projectStateHandler2, times(0)).transform(any(), any(), any());
     }
 
     public void testProcessMultipleChunksDuplicateKeys() throws Exception {
@@ -536,7 +533,7 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
             exceptionRef.get().getMessage(),
             containsString("Failed to merge reserved state chunks because of duplicate keys: [test]")
         );
-        verify(projectStateHandler1, times(0)).transform(any(), any());
+        verify(projectStateHandler1, times(0)).transform(any(), any(), any());
     }
 
     public void testUpdateErrorState() {
@@ -764,10 +761,10 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
         // original state by pointer reference to avoid cluster state update task to run.
         ReservedStateUpdateTask<?> task;
         if (projectId.isPresent()) {
-            ReservedClusterStateHandler<ProjectMetadata, Map<String, Object>> newStateMaker = new TestProjectStateHandler("maker");
-            ReservedClusterStateHandler<ProjectMetadata, Map<String, Object>> exceptionThrower = new TestStateHandler<>("one") {
+            ReservedProjectStateHandler<Map<String, Object>> newStateMaker = new TestProjectStateHandler("maker");
+            ReservedProjectStateHandler<Map<String, Object>> exceptionThrower = new TestProjectStateHandler("one") {
                 @Override
-                public TransformState<ProjectMetadata> transform(Map<String, Object> source, TransformState<ProjectMetadata> prevState)
+                public TransformState transform(ProjectId projectId1, Map<String, Object> source, TransformState prevState)
                     throws Exception {
                     throw new Exception("anything");
                 }
@@ -796,19 +793,13 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
                 )
             );
 
-            var trialRunErrors = controller.trialRun(
-                "namespace_one",
-                state.metadata().getProject(projectId.get()),
-                chunk,
-                new LinkedHashSet<>(orderedHandlers)
-            );
+            var trialRunErrors = controller.trialRun(projectId.get(), "namespace_one", state, chunk, new LinkedHashSet<>(orderedHandlers));
             assertThat(trialRunErrors, contains(containsString("Error processing one state change:")));
         } else {
-            ReservedClusterStateHandler<ClusterState, Map<String, Object>> newStateMaker = new TestClusterStateHandler("maker");
-            ReservedClusterStateHandler<ClusterState, Map<String, Object>> exceptionThrower = new TestStateHandler<>("one") {
+            ReservedClusterStateHandler<Map<String, Object>> newStateMaker = new TestClusterStateHandler("maker");
+            ReservedClusterStateHandler<Map<String, Object>> exceptionThrower = new TestClusterStateHandler("one") {
                 @Override
-                public TransformState<ClusterState> transform(Map<String, Object> source, TransformState<ClusterState> prevState)
-                    throws Exception {
+                public TransformState transform(Map<String, Object> source, TransformState prevState) throws Exception {
                     throw new Exception("anything");
                 }
             };
@@ -940,10 +931,24 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
         assertThat("Cluster state should not be modified", task.execute(state), sameInstance(state));
     }
 
-    private <S> ReservedClusterStateHandler<S, Map<String, Object>> makeHandlerHelper(String name, List<String> deps) {
-        return new TestStateHandler<>(name) {
+    private ReservedClusterStateHandler<Map<String, Object>> makeClusterHandlerHelper(String name, List<String> deps) {
+        return new TestClusterStateHandler(name) {
+            @Override
+            public TransformState transform(Map<String, Object> source, TransformState prevState) throws Exception {
+                return null;
+            }
+
+            @Override
+            public Collection<String> dependencies() {
+                return deps;
+            }
+        };
+    }
+
+    private ReservedProjectStateHandler<Map<String, Object>> makeProjectHandlerHelper(String name, List<String> deps) {
+        return new TestProjectStateHandler(name) {
             @Override
-            public TransformState<S> transform(Map<String, Object> source, TransformState<S> prevState) throws Exception {
+            public TransformState transform(ProjectId projectId, Map<String, Object> source, TransformState prevState) throws Exception {
                 return null;
             }
 
@@ -955,9 +960,9 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
     }
 
     public void testClusterHandlerOrdering() {
-        ReservedClusterStateHandler<ClusterState, Map<String, Object>> oh1 = makeHandlerHelper("one", List.of("two", "three"));
-        ReservedClusterStateHandler<ClusterState, Map<String, Object>> oh2 = makeHandlerHelper("two", List.of());
-        ReservedClusterStateHandler<ClusterState, Map<String, Object>> oh3 = makeHandlerHelper("three", List.of("two"));
+        ReservedClusterStateHandler<Map<String, Object>> oh1 = makeClusterHandlerHelper("one", List.of("two", "three"));
+        ReservedClusterStateHandler<Map<String, Object>> oh2 = makeClusterHandlerHelper("two", List.of());
+        ReservedClusterStateHandler<Map<String, Object>> oh3 = makeClusterHandlerHelper("three", List.of("two"));
 
         ClusterService clusterService = mock(ClusterService.class);
         final var controller = new ReservedClusterStateService(
@@ -983,7 +988,7 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
         );
 
         // Change the second handler so that we create cycle
-        oh2 = makeHandlerHelper("two", List.of("one"));
+        oh2 = makeClusterHandlerHelper("two", List.of("one"));
 
         final var controller1 = new ReservedClusterStateService(clusterService, mock(RerouteService.class), List.of(oh1, oh2), List.of());
 
@@ -997,9 +1002,9 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
     }
 
     public void testProjectHandlerOrdering() {
-        ReservedClusterStateHandler<ProjectMetadata, Map<String, Object>> oh1 = makeHandlerHelper("one", List.of("two", "three"));
-        ReservedClusterStateHandler<ProjectMetadata, Map<String, Object>> oh2 = makeHandlerHelper("two", List.of());
-        ReservedClusterStateHandler<ProjectMetadata, Map<String, Object>> oh3 = makeHandlerHelper("three", List.of("two"));
+        ReservedProjectStateHandler<Map<String, Object>> oh1 = makeProjectHandlerHelper("one", List.of("two", "three"));
+        ReservedProjectStateHandler<Map<String, Object>> oh2 = makeProjectHandlerHelper("two", List.of());
+        ReservedProjectStateHandler<Map<String, Object>> oh3 = makeProjectHandlerHelper("three", List.of("two"));
 
         ClusterService clusterService = mock(ClusterService.class);
         final var controller = new ReservedClusterStateService(
@@ -1025,7 +1030,7 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
         );
 
         // Change the second handler so that we create cycle
-        oh2 = makeHandlerHelper("two", List.of("one"));
+        oh2 = makeProjectHandlerHelper("two", List.of("one"));
 
         final var controller1 = new ReservedClusterStateService(clusterService, mock(RerouteService.class), List.of(), List.of(oh1, oh2));
 
@@ -1052,7 +1057,10 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
                 () -> new ReservedClusterStateService(
                     clusterService,
                     mock(RerouteService.class),
-                    List.of(new ReservedClusterSettingsAction(clusterSettings), new TestStateHandler<>(ReservedClusterSettingsAction.NAME)),
+                    List.of(
+                        new ReservedClusterSettingsAction(clusterSettings),
+                        new TestClusterStateHandler(ReservedClusterSettingsAction.NAME)
+                    ),
                     List.of()
                 )
             ).getMessage(),
@@ -1066,7 +1074,7 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
                     clusterService,
                     mock(RerouteService.class),
                     List.of(new ReservedClusterSettingsAction(clusterSettings)),
-                    List.of(new TestStateHandler<>(ReservedClusterSettingsAction.NAME))
+                    List.of(new TestProjectStateHandler(ReservedClusterSettingsAction.NAME))
                 )
             ).getMessage(),
             startsWith("Duplicate handler name: [cluster_settings]")

+ 4 - 5
x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/LocalStateReservedAutoscalingStateHandlerProvider.java

@@ -7,21 +7,20 @@
 
 package org.elasticsearch.xpack.autoscaling;
 
-import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider;
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
 
 import java.util.Collection;
 
 /**
- * Mock autoscaling provider implementation for the {@link ReservedClusterStateHandlerProvider} service interface
+ * Mock autoscaling provider implementation for the {@link ReservedStateHandlerProvider} service interface
  * <p>
  * This class is a test version of the {@link ReservedAutoscalingStateHandlerProvider}. When we load handler providers through
  * our custom SPI interface, we must match the plugin type exactly. With MockNode, when we run
  * {@link org.elasticsearch.test.ESIntegTestCase} test cases, the version of the {@link Autoscaling} plugin
  * is {@link LocalStateAutoscaling}, therefore we need to provide a test version of this class.
  */
-public class LocalStateReservedAutoscalingStateHandlerProvider implements ReservedClusterStateHandlerProvider {
+public class LocalStateReservedAutoscalingStateHandlerProvider implements ReservedStateHandlerProvider {
     private final LocalStateAutoscaling plugin;
 
     public LocalStateReservedAutoscalingStateHandlerProvider() {
@@ -33,7 +32,7 @@ public class LocalStateReservedAutoscalingStateHandlerProvider implements Reserv
     }
 
     @Override
-    public Collection<ReservedClusterStateHandler<ClusterState, ?>> clusterHandlers() {
+    public Collection<ReservedClusterStateHandler<?>> clusterHandlers() {
         return plugin.testPlugin().reservedClusterStateHandlers();
     }
 }

+ 0 - 0
x-pack/plugin/autoscaling/src/internalClusterTest/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider → x-pack/plugin/autoscaling/src/internalClusterTest/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider


+ 3 - 3
x-pack/plugin/autoscaling/src/main/java/module-info.java

@@ -5,6 +5,8 @@
  * 2.0.
  */
 
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
+
 module org.elasticsearch.autoscaling {
     requires org.apache.logging.log4j;
     requires org.apache.lucene.core;
@@ -18,7 +20,5 @@ module org.elasticsearch.autoscaling {
     exports org.elasticsearch.xpack.autoscaling.capacity;
     exports org.elasticsearch.xpack.autoscaling;
 
-    provides org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider
-        with
-            org.elasticsearch.xpack.autoscaling.ReservedAutoscalingStateHandlerProvider;
+    provides ReservedStateHandlerProvider with org.elasticsearch.xpack.autoscaling.ReservedAutoscalingStateHandlerProvider;
 }

+ 1 - 2
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/Autoscaling.java

@@ -8,7 +8,6 @@
 package org.elasticsearch.xpack.autoscaling;
 
 import org.apache.lucene.util.SetOnce;
-import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.NamedDiff;
 import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
 import org.elasticsearch.cluster.metadata.Metadata;
@@ -231,7 +230,7 @@ public class Autoscaling extends Plugin implements ActionPlugin, ExtensiblePlugi
         return autoscalingExtensions.stream().flatMap(p -> p.deciders().stream()).collect(Collectors.toSet());
     }
 
-    public Collection<ReservedClusterStateHandler<ClusterState, ?>> reservedClusterStateHandlers() {
+    public Collection<ReservedClusterStateHandler<?>> reservedClusterStateHandlers() {
         return Set.of(reservedAutoscalingPolicyAction.get());
     }
 }

+ 4 - 5
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/ReservedAutoscalingStateHandlerProvider.java

@@ -7,16 +7,15 @@
 
 package org.elasticsearch.xpack.autoscaling;
 
-import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider;
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
 
 import java.util.Collection;
 
 /**
- * Autoscaling provider implementation for the {@link ReservedClusterStateHandlerProvider} service interface
+ * Autoscaling provider implementation for the {@link ReservedStateHandlerProvider} service interface
  */
-public class ReservedAutoscalingStateHandlerProvider implements ReservedClusterStateHandlerProvider {
+public class ReservedAutoscalingStateHandlerProvider implements ReservedStateHandlerProvider {
     private final Autoscaling plugin;
 
     public ReservedAutoscalingStateHandlerProvider() {
@@ -28,7 +27,7 @@ public class ReservedAutoscalingStateHandlerProvider implements ReservedClusterS
     }
 
     @Override
-    public Collection<ReservedClusterStateHandler<ClusterState, ?>> clusterHandlers() {
+    public Collection<ReservedClusterStateHandler<?>> clusterHandlers() {
         return plugin.reservedClusterStateHandlers();
     }
 }

+ 3 - 6
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/ReservedAutoscalingPolicyAction.java

@@ -31,9 +31,7 @@ import static org.elasticsearch.common.xcontent.XContentHelper.mapToXContentPars
  * It is used by the ReservedClusterStateService to add/update or remove autoscaling policies. Typical usage
  * for this action is in the context of file based settings.
  */
-public class ReservedAutoscalingPolicyAction
-    implements
-        ReservedClusterStateHandler<ClusterState, List<PutAutoscalingPolicyAction.Request>> {
+public class ReservedAutoscalingPolicyAction implements ReservedClusterStateHandler<List<PutAutoscalingPolicyAction.Request>> {
     public static final String NAME = "autoscaling";
 
     private final AutoscalingCalculateCapacityService.Holder policyValidatorHolder;
@@ -61,8 +59,7 @@ public class ReservedAutoscalingPolicyAction
     }
 
     @Override
-    public TransformState<ClusterState> transform(List<PutAutoscalingPolicyAction.Request> source, TransformState<ClusterState> prevState)
-        throws Exception {
+    public TransformState transform(List<PutAutoscalingPolicyAction.Request> source, TransformState prevState) throws Exception {
         var requests = prepare(source);
         ClusterState state = prevState.state();
 
@@ -79,7 +76,7 @@ public class ReservedAutoscalingPolicyAction
             state = TransportDeleteAutoscalingPolicyAction.deleteAutoscalingPolicy(state, repositoryToDelete);
         }
 
-        return new TransformState<>(state, entities);
+        return new TransformState(state, entities);
 
     }
 

+ 0 - 0
x-pack/plugin/autoscaling/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider → x-pack/plugin/autoscaling/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider


+ 4 - 8
x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/action/ReservedAutoscalingPolicyTests.java

@@ -31,11 +31,7 @@ import static org.mockito.Mockito.mock;
  * Tests that the ReservedAutoscalingPolicyAction does validation, can add and remove autoscaling policies
  */
 public class ReservedAutoscalingPolicyTests extends ESTestCase {
-    private TransformState<ClusterState> processJSON(
-        ReservedAutoscalingPolicyAction action,
-        TransformState<ClusterState> prevState,
-        String json
-    ) throws Exception {
+    private TransformState processJSON(ReservedAutoscalingPolicyAction action, TransformState prevState, String json) throws Exception {
         try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)) {
             return action.transform(action.fromXContent(parser), prevState);
         }
@@ -45,7 +41,7 @@ public class ReservedAutoscalingPolicyTests extends ESTestCase {
         var mocks = createMockServices();
 
         ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build();
-        TransformState<ClusterState> prevState = new TransformState<>(state, Collections.emptySet());
+        TransformState prevState = new TransformState(state, Collections.emptySet());
         ReservedAutoscalingPolicyAction action = new ReservedAutoscalingPolicyAction(mocks);
 
         String badPolicyJSON = """
@@ -76,12 +72,12 @@ public class ReservedAutoscalingPolicyTests extends ESTestCase {
         var mocks = createMockServices();
 
         ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build();
-        TransformState<ClusterState> prevState = new TransformState<>(state, Collections.emptySet());
+        TransformState prevState = new TransformState(state, Collections.emptySet());
         ReservedAutoscalingPolicyAction action = new ReservedAutoscalingPolicyAction(mocks);
 
         String emptyJSON = "";
 
-        TransformState<ClusterState> updatedState = processJSON(action, prevState, emptyJSON);
+        TransformState updatedState = processJSON(action, prevState, emptyJSON);
         assertEquals(0, updatedState.keys().size());
         assertEquals(prevState.state(), updatedState.state());
 

+ 3 - 3
x-pack/plugin/ilm/src/main/java/module-info.java

@@ -1,3 +1,5 @@
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
+
 /*
  * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
  * or more contributor license agreements. Licensed under the Elastic License
@@ -15,7 +17,5 @@ module org.elasticsearch.ilm {
     exports org.elasticsearch.xpack.ilm.action to org.elasticsearch.server;
     exports org.elasticsearch.xpack.ilm;
 
-    provides org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider
-        with
-            org.elasticsearch.xpack.ilm.ReservedLifecycleStateHandlerProvider;
+    provides ReservedStateHandlerProvider with org.elasticsearch.xpack.ilm.ReservedLifecycleStateHandlerProvider;
 }

+ 2 - 3
x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java

@@ -9,7 +9,6 @@ package org.elasticsearch.xpack.ilm;
 import org.apache.lucene.util.SetOnce;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.client.internal.OriginSettingClient;
-import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
 import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.cluster.node.DiscoveryNodes;
@@ -27,7 +26,7 @@ import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.plugins.ActionPlugin;
 import org.elasticsearch.plugins.HealthPlugin;
 import org.elasticsearch.plugins.Plugin;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
 import org.elasticsearch.rest.RestController;
 import org.elasticsearch.rest.RestHandler;
 import org.elasticsearch.xcontent.NamedXContentRegistry;
@@ -292,7 +291,7 @@ public class IndexLifecycle extends Plugin implements ActionPlugin, HealthPlugin
         );
     }
 
-    List<ReservedClusterStateHandler<ClusterState, ?>> reservedClusterStateHandlers() {
+    List<ReservedProjectStateHandler<?>> reservedProjectStateHandlers() {
         return List.of(reservedLifecycleAction.get());
     }
 

+ 6 - 7
x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ReservedLifecycleStateHandlerProvider.java

@@ -7,16 +7,15 @@
 
 package org.elasticsearch.xpack.ilm;
 
-import org.elasticsearch.cluster.ClusterState;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
 
 import java.util.Collection;
 
 /**
- * ILM Provider implementation for the {@link ReservedClusterStateHandlerProvider} service interface
+ * ILM Provider implementation for the {@link ReservedStateHandlerProvider} service interface
  */
-public class ReservedLifecycleStateHandlerProvider implements ReservedClusterStateHandlerProvider {
+public class ReservedLifecycleStateHandlerProvider implements ReservedStateHandlerProvider {
     private final IndexLifecycle plugin;
 
     public ReservedLifecycleStateHandlerProvider() {
@@ -28,7 +27,7 @@ public class ReservedLifecycleStateHandlerProvider implements ReservedClusterSta
     }
 
     @Override
-    public Collection<ReservedClusterStateHandler<ClusterState, ?>> clusterHandlers() {
-        return plugin.reservedClusterStateHandlers();
+    public Collection<ReservedProjectStateHandler<?>> projectHandlers() {
+        return plugin.reservedProjectStateHandlers();
     }
 }

+ 7 - 5
x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/ReservedLifecycleAction.java

@@ -10,9 +10,11 @@ package org.elasticsearch.xpack.ilm.action;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.core.FixForMultiProject;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
 import org.elasticsearch.reservedstate.TransformState;
 import org.elasticsearch.xcontent.NamedXContentRegistry;
 import org.elasticsearch.xcontent.XContentParser;
@@ -40,7 +42,7 @@ import static org.elasticsearch.common.xcontent.XContentHelper.mapToXContentPars
  * {@link TransportDeleteLifecycleAction} to add, update and delete ILM policies.
  */
 @FixForMultiProject // ILM is not a thing on serverless, so this will only ever operate on default project. How do we handle this long-term?
-public class ReservedLifecycleAction implements ReservedClusterStateHandler<ClusterState, List<LifecyclePolicy>> {
+public class ReservedLifecycleAction implements ReservedProjectStateHandler<List<LifecyclePolicy>> {
 
     private final NamedXContentRegistry xContentRegistry;
     private final Client client;
@@ -78,14 +80,14 @@ public class ReservedLifecycleAction implements ReservedClusterStateHandler<Clus
     }
 
     @Override
-    public TransformState<ClusterState> transform(List<LifecyclePolicy> source, TransformState<ClusterState> prevState) throws Exception {
+    public TransformState transform(ProjectId projectId, List<LifecyclePolicy> source, TransformState prevState) throws Exception {
         var requests = prepare(source);
 
         ClusterState state = prevState.state();
 
         for (var request : requests) {
             TransportPutLifecycleAction.UpdateLifecyclePolicyTask task = new TransportPutLifecycleAction.UpdateLifecyclePolicyTask(
-                state.metadata().getProject().id(),
+                state.metadata().getProject(projectId).id(),
                 request,
                 licenseState,
                 xContentRegistry,
@@ -102,7 +104,7 @@ public class ReservedLifecycleAction implements ReservedClusterStateHandler<Clus
 
         for (var policyToDelete : toDelete) {
             TransportDeleteLifecycleAction.DeleteLifecyclePolicyTask task = new TransportDeleteLifecycleAction.DeleteLifecyclePolicyTask(
-                state.metadata().getProject().id(),
+                state.metadata().getProject(projectId).id(),
                 new DeleteLifecycleAction.Request(
                     RESERVED_CLUSTER_STATE_HANDLER_IGNORED_TIMEOUT,
                     RESERVED_CLUSTER_STATE_HANDLER_IGNORED_TIMEOUT,
@@ -113,7 +115,7 @@ public class ReservedLifecycleAction implements ReservedClusterStateHandler<Clus
             state = task.execute(state);
         }
 
-        return new TransformState<>(state, entities);
+        return new TransformState(state, entities);
     }
 
     @Override

+ 0 - 0
x-pack/plugin/ilm/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider → x-pack/plugin/ilm/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider


+ 30 - 24
x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/ReservedLifecycleStateServiceTests.java

@@ -15,6 +15,8 @@ import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.ClusterStateAckListener;
 import org.elasticsearch.cluster.ClusterStateTaskExecutor;
 import org.elasticsearch.cluster.ClusterStateTaskListener;
+import org.elasticsearch.cluster.metadata.ProjectId;
+import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.cluster.service.MasterServiceTaskQueue;
 import org.elasticsearch.common.settings.ClusterSettings;
@@ -121,10 +123,10 @@ public class ReservedLifecycleStateServiceTests extends ESTestCase {
         return new NamedXContentRegistry(entries);
     }
 
-    private TransformState<ClusterState> processJSON(ReservedLifecycleAction action, TransformState<ClusterState> prevState, String json)
+    private TransformState processJSON(ProjectId projectId, ReservedLifecycleAction action, TransformState prevState, String json)
         throws Exception {
         try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)) {
-            return action.transform(action.fromXContent(parser), prevState);
+            return action.transform(projectId, action.fromXContent(parser), prevState);
         }
     }
 
@@ -133,9 +135,11 @@ public class ReservedLifecycleStateServiceTests extends ESTestCase {
         when(client.settings()).thenReturn(Settings.EMPTY);
         final ClusterName clusterName = new ClusterName("elasticsearch");
 
-        ClusterState state = ClusterState.builder(clusterName).build();
+        ProjectId projectId = randomProjectIdOrDefault();
+        ProjectMetadata projectMetadata = ProjectMetadata.builder(projectId).build();
+        ClusterState state = ClusterState.builder(clusterName).putProjectMetadata(projectMetadata).build();
         ReservedLifecycleAction action = new ReservedLifecycleAction(xContentRegistry(), client, mock(XPackLicenseState.class));
-        TransformState<ClusterState> prevState = new TransformState<>(state, Set.of());
+        TransformState prevState = new TransformState(state, Set.of());
 
         String badPolicyJSON = """
             {
@@ -151,7 +155,7 @@ public class ReservedLifecycleStateServiceTests extends ESTestCase {
             }""";
 
         assertThat(
-            expectThrows(XContentParseException.class, () -> processJSON(action, prevState, badPolicyJSON)).getMessage(),
+            expectThrows(XContentParseException.class, () -> processJSON(projectId, action, prevState, badPolicyJSON)).getMessage(),
             is("[1:2] [lifecycle_policy] unknown field [phase] did you mean [phases]?")
         );
     }
@@ -161,15 +165,17 @@ public class ReservedLifecycleStateServiceTests extends ESTestCase {
         when(client.settings()).thenReturn(Settings.EMPTY);
         final ClusterName clusterName = new ClusterName("elasticsearch");
 
-        ClusterState state = ClusterState.builder(clusterName).build();
+        ProjectId projectId = randomProjectIdOrDefault();
+        ProjectMetadata projectMetadata = ProjectMetadata.builder(projectId).build();
+        ClusterState state = ClusterState.builder(clusterName).putProjectMetadata(projectMetadata).build();
 
         ReservedLifecycleAction action = new ReservedLifecycleAction(xContentRegistry(), client, mock(XPackLicenseState.class));
 
         String emptyJSON = "";
 
-        TransformState<ClusterState> prevState = new TransformState<>(state, Set.of());
+        TransformState prevState = new TransformState(state, Set.of());
 
-        TransformState<ClusterState> updatedState = processJSON(action, prevState, emptyJSON);
+        TransformState updatedState = processJSON(projectId, action, prevState, emptyJSON);
         assertThat(updatedState.keys(), empty());
         assertEquals(prevState.state(), updatedState.state());
 
@@ -201,11 +207,11 @@ public class ReservedLifecycleStateServiceTests extends ESTestCase {
             }""";
 
         prevState = updatedState;
-        updatedState = processJSON(action, prevState, twoPoliciesJSON);
+        updatedState = processJSON(projectId, action, prevState, twoPoliciesJSON);
         assertThat(updatedState.keys(), containsInAnyOrder("my_timeseries_lifecycle", "my_timeseries_lifecycle1"));
         IndexLifecycleMetadata ilmMetadata = updatedState.state()
             .metadata()
-            .getProject()
+            .getProject(projectId)
             .custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY);
         assertThat(ilmMetadata.getPolicyMetadatas().keySet(), containsInAnyOrder("my_timeseries_lifecycle", "my_timeseries_lifecycle1"));
 
@@ -223,9 +229,12 @@ public class ReservedLifecycleStateServiceTests extends ESTestCase {
             }""";
 
         prevState = updatedState;
-        updatedState = processJSON(action, prevState, onePolicyRemovedJSON);
+        updatedState = processJSON(projectId, action, prevState, onePolicyRemovedJSON);
         assertThat(updatedState.keys(), containsInAnyOrder("my_timeseries_lifecycle"));
-        ilmMetadata = updatedState.state().metadata().getProject().custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY);
+        ilmMetadata = updatedState.state()
+            .metadata()
+            .getProject(projectId)
+            .custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY);
         assertThat(ilmMetadata.getPolicyMetadatas().keySet(), containsInAnyOrder("my_timeseries_lifecycle"));
 
         String onePolicyRenamedJSON = """
@@ -242,9 +251,12 @@ public class ReservedLifecycleStateServiceTests extends ESTestCase {
             }""";
 
         prevState = updatedState;
-        updatedState = processJSON(action, prevState, onePolicyRenamedJSON);
+        updatedState = processJSON(projectId, action, prevState, onePolicyRenamedJSON);
         assertThat(updatedState.keys(), containsInAnyOrder("my_timeseries_lifecycle2"));
-        ilmMetadata = updatedState.state().metadata().getProject().custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY);
+        ilmMetadata = updatedState.state()
+            .metadata()
+            .getProject(projectId)
+            .custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY);
         assertThat(ilmMetadata.getPolicyMetadatas().keySet(), containsInAnyOrder("my_timeseries_lifecycle2"));
     }
 
@@ -381,11 +393,8 @@ public class ReservedLifecycleStateServiceTests extends ESTestCase {
         controller = new ReservedClusterStateService(
             clusterService,
             null,
-            List.of(
-                new ReservedClusterSettingsAction(clusterSettings),
-                new ReservedLifecycleAction(xContentRegistry(), client, licenseState)
-            ),
-            List.of()
+            List.of(new ReservedClusterSettingsAction(clusterSettings)),
+            List.of(new ReservedLifecycleAction(xContentRegistry(), client, licenseState))
         );
 
         try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) {
@@ -440,11 +449,8 @@ public class ReservedLifecycleStateServiceTests extends ESTestCase {
         controller = new ReservedClusterStateService(
             clusterService,
             null,
-            List.of(
-                new ReservedClusterSettingsAction(clusterSettings),
-                new ReservedLifecycleAction(xContentRegistry(), client, licenseState)
-            ),
-            List.of()
+            List.of(new ReservedClusterSettingsAction(clusterSettings)),
+            List.of(new ReservedLifecycleAction(xContentRegistry(), client, licenseState))
         );
 
         controller.process("operator", pack, randomFrom(ReservedStateVersionCheck.values()), Assert::assertNull);

+ 3 - 3
x-pack/plugin/security/src/main/java/module-info.java

@@ -5,6 +5,8 @@
  * 2.0.
  */
 
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
+
 module org.elasticsearch.security {
     requires java.naming;
     requires java.security.jgss;
@@ -89,9 +91,7 @@ module org.elasticsearch.security {
             org.elasticsearch.xpack.security.authc.file.tool.UsersToolProvider,
             org.elasticsearch.xpack.security.enrollment.tool.AutoConfigGenerateElasticPasswordHashToolProvider;
 
-    provides org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider
-        with
-            org.elasticsearch.xpack.security.ReservedSecurityStateHandlerProvider;
+    provides ReservedStateHandlerProvider with org.elasticsearch.xpack.security.ReservedSecurityStateHandlerProvider;
 
     provides org.elasticsearch.features.FeatureSpecification with org.elasticsearch.xpack.security.SecurityFeatures;
 }

+ 5 - 6
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ReservedSecurityStateHandlerProvider.java

@@ -7,16 +7,15 @@
 
 package org.elasticsearch.xpack.security;
 
-import org.elasticsearch.cluster.metadata.ProjectMetadata;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
 
 import java.util.Collection;
 
 /**
- * Security Provider implementation for the {@link ReservedClusterStateHandlerProvider} service interface
+ * Security Provider implementation for the {@link ReservedStateHandlerProvider} service interface
  */
-public class ReservedSecurityStateHandlerProvider implements ReservedClusterStateHandlerProvider {
+public class ReservedSecurityStateHandlerProvider implements ReservedStateHandlerProvider {
     private final Security plugin;
 
     public ReservedSecurityStateHandlerProvider() {
@@ -28,7 +27,7 @@ public class ReservedSecurityStateHandlerProvider implements ReservedClusterStat
     }
 
     @Override
-    public Collection<ReservedClusterStateHandler<ProjectMetadata, ?>> projectHandlers() {
+    public Collection<ReservedProjectStateHandler<?>> projectHandlers() {
         return plugin.reservedProjectStateHandlers();
     }
 }

+ 2 - 3
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

@@ -31,7 +31,6 @@ import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
 import org.elasticsearch.cluster.metadata.IndexTemplateMetadata;
 import org.elasticsearch.cluster.metadata.ProjectId;
-import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.cluster.node.DiscoveryNodes;
 import org.elasticsearch.cluster.project.ProjectResolver;
@@ -100,7 +99,7 @@ import org.elasticsearch.plugins.ReloadablePlugin;
 import org.elasticsearch.plugins.SearchPlugin;
 import org.elasticsearch.plugins.SystemIndexPlugin;
 import org.elasticsearch.plugins.interceptor.RestServerActionPlugin;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
 import org.elasticsearch.rest.RestController;
 import org.elasticsearch.rest.RestHandler;
 import org.elasticsearch.rest.RestHeaderDefinition;
@@ -2490,7 +2489,7 @@ public class Security extends Plugin
         return this.securityMigrationExecutor.get() != null ? List.of(this.securityMigrationExecutor.get()) : List.of();
     }
 
-    List<ReservedClusterStateHandler<ProjectMetadata, ?>> reservedProjectStateHandlers() {
+    List<ReservedProjectStateHandler<?>> reservedProjectStateHandlers() {
         // If security is disabled we never call the plugin createComponents
         if (enabled == false) {
             return Collections.emptyList();

+ 9 - 7
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/rolemapping/ReservedRoleMappingAction.java

@@ -7,8 +7,10 @@
 
 package org.elasticsearch.xpack.security.action.rolemapping;
 
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.cluster.metadata.ProjectMetadata;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
 import org.elasticsearch.reservedstate.TransformState;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xcontent.XContentParserConfiguration;
@@ -32,7 +34,7 @@ import static org.elasticsearch.common.xcontent.XContentHelper.mapToXContentPars
  * It is used by the ReservedClusterStateService to add/update or remove role mappings. Typical usage
  * for this action is in the context of file based settings.
  */
-public class ReservedRoleMappingAction implements ReservedClusterStateHandler<ProjectMetadata, List<PutRoleMappingRequest>> {
+public class ReservedRoleMappingAction implements ReservedProjectStateHandler<List<PutRoleMappingRequest>> {
     public static final String NAME = "role_mappings";
 
     @Override
@@ -41,19 +43,19 @@ public class ReservedRoleMappingAction implements ReservedClusterStateHandler<Pr
     }
 
     @Override
-    public TransformState<ProjectMetadata> transform(List<PutRoleMappingRequest> source, TransformState<ProjectMetadata> prevState)
-        throws Exception {
+    public TransformState transform(ProjectId projectId, List<PutRoleMappingRequest> source, TransformState prevState) throws Exception {
         Set<ExpressionRoleMapping> roleMappings = validateAndTranslate(source);
+        ProjectMetadata projectMetadata = prevState.state().getMetadata().getProject(projectId);
         RoleMappingMetadata newRoleMappingMetadata = new RoleMappingMetadata(roleMappings);
-        if (newRoleMappingMetadata.equals(RoleMappingMetadata.getFromProject(prevState.state()))) {
+        if (newRoleMappingMetadata.equals(RoleMappingMetadata.getFromProject(projectMetadata))) {
             return prevState;
         } else {
-            ProjectMetadata newState = newRoleMappingMetadata.updateProject(prevState.state());
+            ProjectMetadata newProjectMetadata = newRoleMappingMetadata.updateProject(projectMetadata);
             Set<String> entities = newRoleMappingMetadata.getRoleMappings()
                 .stream()
                 .map(ExpressionRoleMapping::getName)
                 .collect(Collectors.toSet());
-            return new TransformState<>(newState, entities);
+            return new TransformState(ClusterState.builder(prevState.state()).putProjectMetadata(newProjectMetadata).build(), entities);
         }
     }
 

+ 0 - 0
x-pack/plugin/security/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider → x-pack/plugin/security/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider


+ 5 - 6
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalReservedSecurityStateHandlerProvider.java

@@ -7,20 +7,19 @@
 
 package org.elasticsearch.xpack.security;
 
-import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.plugins.Plugin;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider;
+import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
 
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Objects;
 
 /**
- * Mock Security Provider implementation for the {@link ReservedClusterStateHandlerProvider} service interface. This is used
+ * Mock Security Provider implementation for the {@link ReservedStateHandlerProvider} service interface. This is used
  * for {@link org.elasticsearch.test.ESIntegTestCase} because the Security Plugin is really LocalStateSecurity in those tests.
  */
-public class LocalReservedSecurityStateHandlerProvider implements ReservedClusterStateHandlerProvider {
+public class LocalReservedSecurityStateHandlerProvider implements ReservedStateHandlerProvider {
     protected final LocalStateSecurity plugin;
 
     public LocalReservedSecurityStateHandlerProvider() {
@@ -45,7 +44,7 @@ public class LocalReservedSecurityStateHandlerProvider implements ReservedCluste
     }
 
     @Override
-    public Collection<ReservedClusterStateHandler<ProjectMetadata, ?>> projectHandlers() {
+    public Collection<ReservedProjectStateHandler<?>> projectHandlers() {
         for (Plugin subPlugin : plugin.plugins()) {
             if (subPlugin instanceof Security security) {
                 return security.reservedProjectStateHandlers();

+ 22 - 14
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/reservedstate/ReservedRoleMappingActionTests.java

@@ -7,6 +7,9 @@
 
 package org.elasticsearch.xpack.security.action.reservedstate;
 
+import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.common.ParsingException;
 import org.elasticsearch.reservedstate.TransformState;
@@ -26,20 +29,21 @@ import static org.hamcrest.Matchers.empty;
  */
 public class ReservedRoleMappingActionTests extends ESTestCase {
 
-    private TransformState<ProjectMetadata> processJSON(
-        ReservedRoleMappingAction action,
-        TransformState<ProjectMetadata> prevState,
-        String json
-    ) throws Exception {
+    private TransformState processJSON(ProjectId projectId, ReservedRoleMappingAction action, TransformState prevState, String json)
+        throws Exception {
         try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)) {
             var content = action.fromXContent(parser);
-            return action.transform(content, prevState);
+            return action.transform(projectId, content, prevState);
         }
     }
 
     public void testValidation() {
-        ProjectMetadata state = ProjectMetadata.builder(randomProjectIdOrDefault()).build();
-        TransformState<ProjectMetadata> prevState = new TransformState<>(state, Collections.emptySet());
+        ProjectId projectId = randomProjectIdOrDefault();
+        ProjectMetadata project = ProjectMetadata.builder(projectId).build();
+        TransformState prevState = new TransformState(
+            ClusterState.builder(ClusterName.DEFAULT).putProjectMetadata(project).build(),
+            Collections.emptySet()
+        );
         ReservedRoleMappingAction action = new ReservedRoleMappingAction();
         String badPolicyJSON = """
             {
@@ -62,17 +66,21 @@ public class ReservedRoleMappingActionTests extends ESTestCase {
             }""";
         assertEquals(
             "failed to parse role-mapping [everyone_fleet]. missing field [rules]",
-            expectThrows(ParsingException.class, () -> processJSON(action, prevState, badPolicyJSON)).getMessage()
+            expectThrows(ParsingException.class, () -> processJSON(projectId, action, prevState, badPolicyJSON)).getMessage()
         );
     }
 
     public void testAddRemoveRoleMapping() throws Exception {
-        ProjectMetadata state = ProjectMetadata.builder(randomProjectIdOrDefault()).build();
-        TransformState<ProjectMetadata> prevState = new TransformState<>(state, Collections.emptySet());
+        ProjectId projectId = randomProjectIdOrDefault();
+        ProjectMetadata project = ProjectMetadata.builder(projectId).build();
+        TransformState prevState = new TransformState(
+            ClusterState.builder(ClusterName.DEFAULT).putProjectMetadata(project).build(),
+            Collections.emptySet()
+        );
         ReservedRoleMappingAction action = new ReservedRoleMappingAction();
         String emptyJSON = "";
 
-        TransformState<ProjectMetadata> updatedState = processJSON(action, prevState, emptyJSON);
+        TransformState updatedState = processJSON(projectId, action, prevState, emptyJSON);
         assertThat(updatedState.keys(), empty());
         assertEquals(prevState.state(), updatedState.state());
 
@@ -99,10 +107,10 @@ public class ReservedRoleMappingActionTests extends ESTestCase {
             }""";
 
         prevState = updatedState;
-        updatedState = processJSON(action, prevState, json);
+        updatedState = processJSON(projectId, action, prevState, json);
         assertThat(updatedState.keys(), containsInAnyOrder("everyone_kibana", "everyone_fleet"));
 
-        updatedState = processJSON(action, prevState, emptyJSON);
+        updatedState = processJSON(projectId, action, prevState, emptyJSON);
         assertThat(updatedState.keys(), empty());
     }
 }

+ 0 - 0
x-pack/plugin/security/src/test/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider → x-pack/plugin/security/src/test/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider


+ 3 - 3
x-pack/plugin/slm/src/main/java/module-info.java

@@ -1,3 +1,5 @@
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
+
 /*
  * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
  * or more contributor license agreements. Licensed under the Elastic License
@@ -15,9 +17,7 @@ module org.elasticsearch.slm {
     exports org.elasticsearch.xpack.slm.action to org.elasticsearch.server;
     exports org.elasticsearch.xpack.slm to org.elasticsearch.server;
 
-    provides org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider
-        with
-            org.elasticsearch.xpack.slm.ReservedLifecycleStateHandlerProvider;
+    provides ReservedStateHandlerProvider with org.elasticsearch.xpack.slm.ReservedLifecycleStateHandlerProvider;
 
     provides org.elasticsearch.features.FeatureSpecification with org.elasticsearch.xpack.slm.SnapshotLifecycleFeatures;
 }

+ 4 - 5
x-pack/plugin/slm/src/main/java/org/elasticsearch/xpack/slm/ReservedLifecycleStateHandlerProvider.java

@@ -7,16 +7,15 @@
 
 package org.elasticsearch.xpack.slm;
 
-import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
-import org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider;
+import org.elasticsearch.reservedstate.ReservedStateHandlerProvider;
 
 import java.util.Collection;
 
 /**
- * SLM Provider implementation for the {@link ReservedClusterStateHandlerProvider} service interface
+ * SLM Provider implementation for the {@link ReservedStateHandlerProvider} service interface
  */
-public class ReservedLifecycleStateHandlerProvider implements ReservedClusterStateHandlerProvider {
+public class ReservedLifecycleStateHandlerProvider implements ReservedStateHandlerProvider {
     private final SnapshotLifecycle plugin;
 
     public ReservedLifecycleStateHandlerProvider() {
@@ -28,7 +27,7 @@ public class ReservedLifecycleStateHandlerProvider implements ReservedClusterSta
     }
 
     @Override
-    public Collection<ReservedClusterStateHandler<ClusterState, ?>> clusterHandlers() {
+    public Collection<ReservedClusterStateHandler<?>> clusterHandlers() {
         return plugin.reservedClusterStateHandlers();
     }
 }

+ 1 - 2
x-pack/plugin/slm/src/main/java/org/elasticsearch/xpack/slm/SnapshotLifecycle.java

@@ -10,7 +10,6 @@ import org.apache.lucene.util.SetOnce;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.client.internal.OriginSettingClient;
-import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
 import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.cluster.node.DiscoveryNodes;
@@ -233,7 +232,7 @@ public class SnapshotLifecycle extends Plugin implements ActionPlugin, HealthPlu
         return actions;
     }
 
-    List<ReservedClusterStateHandler<ClusterState, ?>> reservedClusterStateHandlers() {
+    List<ReservedClusterStateHandler<?>> reservedClusterStateHandlers() {
         return List.of(new ReservedSnapshotAction());
     }
 

+ 3 - 4
x-pack/plugin/slm/src/main/java/org/elasticsearch/xpack/slm/action/ReservedSnapshotAction.java

@@ -36,7 +36,7 @@ import static org.elasticsearch.common.xcontent.XContentHelper.mapToXContentPars
  * Internally it uses {@link TransportPutSnapshotLifecycleAction} and
  * {@link TransportDeleteSnapshotLifecycleAction} to add, update and delete ILM policies.
  */
-public class ReservedSnapshotAction implements ReservedClusterStateHandler<ClusterState, List<SnapshotLifecyclePolicy>> {
+public class ReservedSnapshotAction implements ReservedClusterStateHandler<List<SnapshotLifecyclePolicy>> {
 
     public static final String NAME = "slm";
 
@@ -77,8 +77,7 @@ public class ReservedSnapshotAction implements ReservedClusterStateHandler<Clust
     }
 
     @Override
-    public TransformState<ClusterState> transform(List<SnapshotLifecyclePolicy> source, TransformState<ClusterState> prevState)
-        throws Exception {
+    public TransformState transform(List<SnapshotLifecyclePolicy> source, TransformState prevState) throws Exception {
         var requests = prepare(source, prevState.state());
 
         ClusterState state = prevState.state();
@@ -107,7 +106,7 @@ public class ReservedSnapshotAction implements ReservedClusterStateHandler<Clust
             state = task.execute(state);
         }
 
-        return new TransformState<>(state, entities);
+        return new TransformState(state, entities);
     }
 
     @Override

+ 0 - 0
x-pack/plugin/slm/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider → x-pack/plugin/slm/src/main/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedStateHandlerProvider


+ 4 - 5
x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/action/ReservedSnapshotLifecycleStateServiceTests.java

@@ -72,8 +72,7 @@ import static org.mockito.Mockito.when;
  */
 public class ReservedSnapshotLifecycleStateServiceTests extends ESTestCase {
 
-    private TransformState<ClusterState> processJSON(ReservedSnapshotAction action, TransformState<ClusterState> prevState, String json)
-        throws Exception {
+    private TransformState processJSON(ReservedSnapshotAction action, TransformState prevState, String json) throws Exception {
         try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)) {
             return action.transform(action.fromXContent(parser), prevState);
         }
@@ -91,7 +90,7 @@ public class ReservedSnapshotLifecycleStateServiceTests extends ESTestCase {
 
         ClusterState state = ClusterState.builder(clusterName).build();
         ReservedSnapshotAction action = new ReservedSnapshotAction();
-        TransformState<ClusterState> prevState = new TransformState<>(state, Set.of());
+        TransformState prevState = new TransformState(state, Set.of());
 
         String badPolicyJSON = """
             {
@@ -133,9 +132,9 @@ public class ReservedSnapshotLifecycleStateServiceTests extends ESTestCase {
 
         String emptyJSON = "";
 
-        TransformState<ClusterState> prevState = new TransformState<>(state, Set.of());
+        TransformState prevState = new TransformState(state, Set.of());
 
-        TransformState<ClusterState> updatedState = processJSON(action, prevState, emptyJSON);
+        TransformState updatedState = processJSON(action, prevState, emptyJSON);
         assertThat(updatedState.keys(), empty());
         assertEquals(prevState.state(), updatedState.state());