1414import org .elasticsearch .action .ActionListener ;
1515import org .elasticsearch .action .admin .cluster .snapshots .features .ResetFeatureStateRequest ;
1616import org .elasticsearch .action .admin .cluster .snapshots .features .TransportResetFeatureStateAction ;
17+ import org .elasticsearch .action .admin .indices .delete .DeleteIndexRequest ;
1718import org .elasticsearch .action .search .SearchRequest ;
1819import org .elasticsearch .action .support .IndicesOptions ;
1920import org .elasticsearch .action .support .master .AcknowledgedResponse ;
3738import org .elasticsearch .node .NodeRoleSettings ;
3839import org .elasticsearch .plugins .EnginePlugin ;
3940import org .elasticsearch .plugins .Plugin ;
41+ import org .elasticsearch .reindex .ReindexPlugin ;
4042import org .elasticsearch .search .SearchModule ;
4143import org .elasticsearch .search .aggregations .BaseAggregationBuilder ;
4244import org .elasticsearch .search .builder .SearchSourceBuilder ;
5254import org .elasticsearch .xpack .core .transform .MockDeprecatedAggregationBuilder ;
5355import org .elasticsearch .xpack .core .transform .MockDeprecatedQueryBuilder ;
5456import org .elasticsearch .xpack .core .transform .TransformNamedXContentProvider ;
57+ import org .elasticsearch .xpack .core .transform .action .DeleteTransformAction ;
5558import org .elasticsearch .xpack .core .transform .action .GetCheckpointAction ;
5659import org .elasticsearch .xpack .core .transform .action .GetTransformStatsAction ;
5760import org .elasticsearch .xpack .core .transform .action .PutTransformAction ;
5861import org .elasticsearch .xpack .core .transform .action .StartTransformAction ;
62+ import org .elasticsearch .xpack .core .transform .action .StopTransformAction ;
63+ import org .elasticsearch .xpack .core .transform .action .UpdateTransformAction ;
5964import org .elasticsearch .xpack .core .transform .transforms .DestConfig ;
6065import org .elasticsearch .xpack .core .transform .transforms .QueryConfig ;
6166import org .elasticsearch .xpack .core .transform .transforms .SourceConfig ;
67+ import org .elasticsearch .xpack .core .transform .transforms .TimeSyncConfig ;
6268import org .elasticsearch .xpack .core .transform .transforms .TransformConfig ;
69+ import org .elasticsearch .xpack .core .transform .transforms .TransformConfigUpdate ;
6370import org .elasticsearch .xpack .core .transform .transforms .TransformStats ;
6471import org .elasticsearch .xpack .core .transform .transforms .latest .LatestConfig ;
6572import org .elasticsearch .xpack .transform .LocalStateTransform ;
8491import static org .hamcrest .Matchers .equalTo ;
8592import static org .hamcrest .Matchers .hasSize ;
8693import static org .hamcrest .Matchers .is ;
94+ import static org .hamcrest .Matchers .notNullValue ;
8795import static org .hamcrest .Matchers .nullValue ;
8896
8997public class TransformCCSCanMatchIT extends AbstractMultiClustersTestCase {
@@ -392,6 +400,72 @@ private void testTransformLifecycle(QueryBuilder query, long expectedHitCount) t
392400 });
393401 }
394402
403+ public void testUpdateTransformCreatesDestIndexWhenRunningAndDestMissingWithRemoteSource () throws Exception {
404+ String transformId = "test-ccs-transform-update-dest" ;
405+ String destIndex = transformId + "-dest" ;
406+
407+ // Create a continuous transform sourced from both local and remote indices (CCS).
408+ // ZERO delay ensures the already-indexed documents are included in the first checkpoint.
409+ TransformConfig transformConfig = TransformConfig .builder ()
410+ .setId (transformId )
411+ .setSource (new SourceConfig (new String [] { "local_*" , "*:remote_*" }, QueryConfig .matchAll (), Map .of (), null ))
412+ .setDest (new DestConfig (destIndex , null , null ))
413+ .setFrequency (TimeValue .timeValueMinutes (1 ))
414+ .setSyncConfig (new TimeSyncConfig ("@timestamp" , TimeValue .ZERO ))
415+ .setLatestConfig (new LatestConfig (List .of ("position" ), "@timestamp" ))
416+ .build ();
417+
418+ client ().execute (PutTransformAction .INSTANCE , new PutTransformAction .Request (transformConfig , false , TIMEOUT )).actionGet (TIMEOUT );
419+ client ().execute (StartTransformAction .INSTANCE , new StartTransformAction .Request (transformId , null , TIMEOUT )).actionGet (TIMEOUT );
420+
421+ // Wait for the first checkpoint to create the destination index.
422+ assertBusy (
423+ () -> assertThat (
424+ "dest index should be created after the first checkpoint" ,
425+ cluster (LOCAL_CLUSTER ).clusterService ().state ().metadata ().getProject ().index (destIndex ),
426+ is (notNullValue ())
427+ )
428+ );
429+
430+ // Delete the destination index to simulate it going missing while the transform is still running.
431+ client (LOCAL_CLUSTER ).admin ().indices ().delete (new DeleteIndexRequest (destIndex )).actionGet (TIMEOUT );
432+ assertThat (cluster (LOCAL_CLUSTER ).clusterService ().state ().metadata ().getProject ().index (destIndex ), is (nullValue ()));
433+
434+ // Update the transform. A description change ensures we go through updateTransformConfiguration
435+ // (an EMPTY update against a current-version config returns NONE early without touching the dest index).
436+ client ().execute (
437+ UpdateTransformAction .INSTANCE ,
438+ new UpdateTransformAction .Request (
439+ new TransformConfigUpdate (null , null , null , null , "post-delete-update" , null , null , null ),
440+ transformId ,
441+ false ,
442+ TIMEOUT
443+ )
444+ ).actionGet (TIMEOUT );
445+
446+ // The update should have invoked resolveSourceIndicesAndCreateDestIfNeeded, which calls ResolveIndexAction
447+ // to resolve the remote CCS source indices and then recreates the destination index.
448+ assertThat (
449+ "update should have recreated the destination index using ResolveIndexAction for CCS source resolution" ,
450+ cluster (LOCAL_CLUSTER ).clusterService ().state ().metadata ().getProject ().index (destIndex ),
451+ is (notNullValue ())
452+ );
453+
454+ // Cleanup
455+ stopTransform (transformId );
456+ deleteTransform (transformId );
457+ }
458+
459+ private void stopTransform (String transformId ) {
460+ client ().execute (StopTransformAction .INSTANCE , new StopTransformAction .Request (transformId , true , true , TIMEOUT , false , false ))
461+ .actionGet (TIMEOUT );
462+ }
463+
464+ private void deleteTransform (String transformId ) {
465+ client ().execute (DeleteTransformAction .INSTANCE , new DeleteTransformAction .Request (transformId , true , false , TIMEOUT ))
466+ .actionGet (TIMEOUT );
467+ }
468+
395469 @ Override
396470 protected NamedXContentRegistry xContentRegistry () {
397471 return namedXContentRegistry ;
@@ -405,7 +479,10 @@ protected List<String> remoteClusterAlias() {
405479 @ Override
406480 protected Collection <Class <? extends Plugin >> nodePlugins (String clusterAlias ) {
407481 return CollectionUtils .appendToCopy (
408- CollectionUtils .appendToCopy (super .nodePlugins (clusterAlias ), LocalStateTransform .class ),
482+ CollectionUtils .appendToCopy (
483+ CollectionUtils .appendToCopy (super .nodePlugins (clusterAlias ), LocalStateTransform .class ),
484+ ReindexPlugin .class
485+ ),
409486 ExposingTimestampEnginePlugin .class
410487 );
411488 }
0 commit comments