1414import org .elasticsearch .action .ActionRequestValidationException ;
1515import org .elasticsearch .action .ActionResponse ;
1616import org .elasticsearch .action .IndicesRequest ;
17+ import org .elasticsearch .action .admin .cluster .settings .ClusterUpdateSettingsRequest ;
1718import org .elasticsearch .action .support .ActionFilters ;
1819import org .elasticsearch .action .support .ActionTestUtils ;
1920import org .elasticsearch .action .support .IndicesOptions ;
2021import org .elasticsearch .action .support .PlainActionFuture ;
2122import org .elasticsearch .action .support .ThreadedActionListener ;
2223import org .elasticsearch .action .support .replication .ClusterStateCreationUtils ;
24+ import org .elasticsearch .cluster .ClusterName ;
2325import org .elasticsearch .cluster .ClusterState ;
2426import org .elasticsearch .cluster .NotMasterException ;
2527import org .elasticsearch .cluster .block .ClusterBlock ;
3032import org .elasticsearch .cluster .metadata .IndexMetadata ;
3133import org .elasticsearch .cluster .metadata .IndexNameExpressionResolver ;
3234import org .elasticsearch .cluster .metadata .Metadata ;
35+ import org .elasticsearch .cluster .metadata .ReservedStateHandlerMetadata ;
36+ import org .elasticsearch .cluster .metadata .ReservedStateMetadata ;
3337import org .elasticsearch .cluster .node .DiscoveryNode ;
3438import org .elasticsearch .cluster .node .DiscoveryNodeRole ;
3539import org .elasticsearch .cluster .node .DiscoveryNodes ;
4347import org .elasticsearch .discovery .MasterNotDiscoveredException ;
4448import org .elasticsearch .indices .TestIndexNameExpressionResolver ;
4549import org .elasticsearch .node .NodeClosedException ;
50+ import org .elasticsearch .reservedstate .action .ReservedClusterSettingsAction ;
4651import org .elasticsearch .rest .RestStatus ;
4752import org .elasticsearch .tasks .CancellableTask ;
4853import org .elasticsearch .tasks .Task ;
6570import java .util .HashSet ;
6671import java .util .Map ;
6772import java .util .Objects ;
73+ import java .util .Optional ;
6874import java .util .Set ;
6975import java .util .concurrent .CountDownLatch ;
7076import java .util .concurrent .CyclicBarrier ;
@@ -254,6 +260,63 @@ protected ClusterBlockException checkBlock(Request request, ClusterState state)
254260 }
255261 }
256262
263+ class ReservedStateAction extends Action {
264+ ReservedStateAction (String actionName , TransportService transportService , ClusterService clusterService , ThreadPool threadPool ) {
265+ super (actionName , transportService , clusterService , threadPool , ThreadPool .Names .SAME );
266+ }
267+
268+ @ Override
269+ protected Optional <String > reservedStateHandlerName () {
270+ return Optional .of ("test_reserved_state_action" );
271+ }
272+ }
273+
274+ class FakeClusterStateUpdateAction extends TransportMasterNodeAction <ClusterUpdateSettingsRequest , Response > {
275+ FakeClusterStateUpdateAction (
276+ String actionName ,
277+ TransportService transportService ,
278+ ClusterService clusterService ,
279+ ThreadPool threadPool ,
280+ String executor
281+ ) {
282+ super (
283+ actionName ,
284+ transportService ,
285+ clusterService ,
286+ threadPool ,
287+ new ActionFilters (new HashSet <>()),
288+ ClusterUpdateSettingsRequest ::new ,
289+ TestIndexNameExpressionResolver .newInstance (),
290+ Response ::new ,
291+ executor
292+ );
293+ }
294+
295+ @ Override
296+ protected void masterOperation (
297+ Task task ,
298+ ClusterUpdateSettingsRequest request ,
299+ ClusterState state ,
300+ ActionListener <Response > listener
301+ ) {}
302+
303+ @ Override
304+ protected ClusterBlockException checkBlock (ClusterUpdateSettingsRequest request , ClusterState state ) {
305+ return null ;
306+ }
307+
308+ @ Override
309+ protected Optional <String > reservedStateHandlerName () {
310+ return Optional .of (ReservedClusterSettingsAction .NAME );
311+ }
312+
313+ @ Override
314+ protected Set <String > modifiedKeys (ClusterUpdateSettingsRequest request ) {
315+ Settings allSettings = Settings .builder ().put (request .persistentSettings ()).put (request .transientSettings ()).build ();
316+ return allSettings .keySet ();
317+ }
318+ }
319+
257320 public void testLocalOperationWithoutBlocks () throws ExecutionException , InterruptedException {
258321 final boolean masterOperationFailure = randomBoolean ();
259322
@@ -686,7 +749,6 @@ protected ClusterBlockException checkBlock(Request request, ClusterState state)
686749 indexNameExpressionResolver .concreteIndexNamesWithSystemIndexAccess (state , request )
687750 );
688751 }
689-
690752 };
691753
692754 PlainActionFuture <Response > listener = new PlainActionFuture <>();
@@ -697,6 +759,54 @@ protected ClusterBlockException checkBlock(Request request, ClusterState state)
697759 assertThat (ex .getCause ().getCause (), instanceOf (ClusterBlockException .class ));
698760 }
699761
762+ public void testRejectImmutableConflictClusterStateUpdate () {
763+ ReservedStateHandlerMetadata hmOne = new ReservedStateHandlerMetadata (ReservedClusterSettingsAction .NAME , Set .of ("a" , "b" ));
764+ ReservedStateHandlerMetadata hmThree = new ReservedStateHandlerMetadata (ReservedClusterSettingsAction .NAME , Set .of ("e" , "f" ));
765+ ReservedStateMetadata omOne = ReservedStateMetadata .builder ("namespace_one" ).putHandler (hmOne ).build ();
766+ ReservedStateMetadata omTwo = ReservedStateMetadata .builder ("namespace_two" ).putHandler (hmThree ).build ();
767+
768+ Metadata metadata = Metadata .builder ().put (omOne ).put (omTwo ).build ();
769+
770+ ClusterState clusterState = ClusterState .builder (new ClusterName ("test" )).metadata (metadata ).build ();
771+
772+ Action noHandler = new Action ("internal:testAction" , transportService , clusterService , threadPool , ThreadPool .Names .SAME );
773+
774+ assertFalse (noHandler .supportsImmutableState ());
775+
776+ noHandler = new ReservedStateAction ("internal:testOpAction" , transportService , clusterService , threadPool );
777+
778+ assertTrue (noHandler .supportsImmutableState ());
779+
780+ // nothing should happen here, since the request doesn't touch any of the immutable state keys
781+ noHandler .validateForImmutableState (new Request (), clusterState );
782+
783+ ClusterUpdateSettingsRequest request = new ClusterUpdateSettingsRequest ().persistentSettings (
784+ Settings .builder ().put ("a" , "a value" ).build ()
785+ ).transientSettings (Settings .builder ().put ("e" , "e value" ).build ());
786+
787+ FakeClusterStateUpdateAction action = new FakeClusterStateUpdateAction (
788+ "internal:testClusterSettings" ,
789+ transportService ,
790+ clusterService ,
791+ threadPool ,
792+ ThreadPool .Names .SAME
793+ );
794+
795+ assertTrue (action .supportsImmutableState ());
796+
797+ assertTrue (
798+ expectThrows (IllegalArgumentException .class , () -> action .validateForImmutableState (request , clusterState )).getMessage ()
799+ .contains ("with errors: [[a] set as read-only by [namespace_one], " + "[e] set as read-only by [namespace_two]" )
800+ );
801+
802+ ClusterUpdateSettingsRequest okRequest = new ClusterUpdateSettingsRequest ().persistentSettings (
803+ Settings .builder ().put ("m" , "m value" ).build ()
804+ ).transientSettings (Settings .builder ().put ("n" , "n value" ).build ());
805+
806+ // this should just work, no conflicts
807+ action .validateForImmutableState (okRequest , clusterState );
808+ }
809+
700810 private Runnable blockAllThreads (String executorName ) throws Exception {
701811 final int numberOfThreads = threadPool .info (executorName ).getMax ();
702812 final EsThreadPoolExecutor executor = (EsThreadPoolExecutor ) threadPool .executor (executorName );
0 commit comments