|
95 | 95 | import org.opensearch.timeseries.util.ParseUtils; |
96 | 96 | import org.opensearch.timeseries.util.RestHandlerUtils; |
97 | 97 | import org.opensearch.timeseries.util.SecurityClientUtil; |
98 | | -import org.opensearch.transport.TransportService; |
| 98 | +import org.opensearch.transport.*; |
99 | 99 | import org.opensearch.transport.client.Client; |
100 | 100 |
|
101 | 101 | import com.google.common.collect.Sets; |
@@ -156,6 +156,7 @@ public abstract class AbstractTimeSeriesActionHandler<T extends ActionResponse, |
156 | 156 | protected final ConfigUpdateConfirmer<IndexType, IndexManagementType, TaskCacheManagerType, TaskTypeEnum, TaskClass, TaskManagerType> handler; |
157 | 157 | protected final ClusterService clusterService; |
158 | 158 | protected final NamedXContentRegistry xContentRegistry; |
| 159 | + protected final TransportService transportService; |
159 | 160 | protected final TimeValue requestTimeout; |
160 | 161 | protected final WriteRequest.RefreshPolicy refreshPolicy; |
161 | 162 | protected final Long seqNo; |
@@ -217,6 +218,7 @@ public AbstractTimeSeriesActionHandler( |
217 | 218 | this.method = method; |
218 | 219 | this.clusterService = clusterService; |
219 | 220 | this.xContentRegistry = xContentRegistry; |
| 221 | + this.transportService = transportService; |
220 | 222 | this.requestTimeout = requestTimeout; |
221 | 223 | this.refreshPolicy = refreshPolicy; |
222 | 224 | this.seqNo = seqNo; |
@@ -321,13 +323,86 @@ protected void validateName(boolean indexingDryRun, ActionListener<T> listener) |
321 | 323 | listener.onFailure(createValidationException(AbstractTimeSeriesActionHandler.INVALID_NAME_SIZE, ValidationIssueType.NAME)); |
322 | 324 | return; |
323 | 325 | } |
324 | | - validateTimeField(indexingDryRun, listener); |
| 326 | + validateTimeFieldInAllClusters(indexingDryRun, listener); |
325 | 327 | } |
326 | 328 |
|
327 | | - protected void validateTimeField(boolean indexingDryRun, ActionListener<T> listener) { |
| 329 | + protected void validateTimeFieldInAllClusters(boolean indexingDryRun, ActionListener<T> listener) { |
| 330 | + List<String> wildcardClusterIndices = config.getIndices().stream().filter(idx -> idx.startsWith("*:")).toList(); |
| 331 | + List<String> nonWildcardClusterIndices = config.getIndices().stream().filter(idx -> !idx.startsWith("*:")).toList(); |
| 332 | + |
| 333 | + Map<String, List<String>> clusterIndicesMap = new HashMap<>(); |
| 334 | + |
| 335 | + // if no wildcard cluster indices found, fallback to typical validation |
| 336 | + if (wildcardClusterIndices.isEmpty()) { |
| 337 | + if (!nonWildcardClusterIndices.isEmpty()) { |
| 338 | + HashMap<String, List<String>> nonWildcardMap = CrossClusterConfigUtils |
| 339 | + .separateClusterIndexes(nonWildcardClusterIndices, clusterService); |
| 340 | + clusterIndicesMap.putAll(nonWildcardMap); |
| 341 | + validateTimeField(clusterIndicesMap, indexingDryRun, listener); |
| 342 | + } else { |
| 343 | + listener |
| 344 | + .onFailure( |
| 345 | + new ValidationException( |
| 346 | + "No indices specified for time field validation.", |
| 347 | + ValidationIssueType.TIMEFIELD_FIELD, |
| 348 | + configValidationAspect |
| 349 | + ) |
| 350 | + ); |
| 351 | + } |
| 352 | + return; |
| 353 | + } |
| 354 | + |
| 355 | + // if there are wildcard cluster indices, check for remote clusters. throw exception if none configured |
| 356 | + RemoteClusterService remoteClusterService = transportService.getRemoteClusterService(); |
| 357 | + Set<String> remoteClusters = remoteClusterService.getRegisteredRemoteClusterNames(); |
| 358 | + |
| 359 | + if (remoteClusters.isEmpty()) { |
| 360 | + listener |
| 361 | + .onFailure( |
| 362 | + new ValidationException( |
| 363 | + "Indices with wildcard cluster prefix specified, but no remote clusters are configured. Please configure remote clusters or remove the wildcard cluster prefix from the indices.", |
| 364 | + ValidationIssueType.TIMEFIELD_FIELD, |
| 365 | + configValidationAspect |
| 366 | + ) |
| 367 | + ); |
| 368 | + return; |
| 369 | + } |
| 370 | + |
| 371 | + // For each remote cluster, add all indices matching the wildcard pattern |
| 372 | + for (String remote : remoteClusters) { |
| 373 | + List<String> remoteIndices = wildcardClusterIndices |
| 374 | + .stream() |
| 375 | + .map(idx -> idx.substring(2)) // remove "*:" prefix |
| 376 | + .toList(); |
| 377 | + |
| 378 | + if (!remoteIndices.isEmpty()) { |
| 379 | + clusterIndicesMap.put(remote, remoteIndices); |
| 380 | + } |
| 381 | + } |
| 382 | + |
| 383 | + // add non-wildcard indices (local or explicit remote) to the map |
| 384 | + if (!nonWildcardClusterIndices.isEmpty()) { |
| 385 | + HashMap<String, List<String>> nonWildcardIndicesMap = CrossClusterConfigUtils |
| 386 | + .separateClusterIndexes(nonWildcardClusterIndices, clusterService); |
| 387 | + clusterIndicesMap.putAll(nonWildcardIndicesMap); |
| 388 | + } |
| 389 | + |
| 390 | + if (clusterIndicesMap.isEmpty()) { |
| 391 | + listener |
| 392 | + .onFailure( |
| 393 | + new ValidationException( |
| 394 | + "No indices found for time field validation.", |
| 395 | + ValidationIssueType.TIMEFIELD_FIELD, |
| 396 | + configValidationAspect |
| 397 | + ) |
| 398 | + ); |
| 399 | + } else { |
| 400 | + validateTimeField(clusterIndicesMap, indexingDryRun, listener); |
| 401 | + } |
| 402 | + } |
| 403 | + |
| 404 | + protected void validateTimeField(Map<String, List<String>> clusterIndicesMap, boolean indexingDryRun, ActionListener<T> listener) { |
328 | 405 | String givenTimeField = config.getTimeField(); |
329 | | - HashMap<String, List<String>> clusterIndicesMap = CrossClusterConfigUtils |
330 | | - .separateClusterIndexes(config.getIndices(), clusterService); |
331 | 406 |
|
332 | 407 | ActionListener<MergeableList<Optional<double[]>>> validateGetMappingForTimeFieldListener = ActionListener.wrap(response -> { |
333 | 408 | prepareConfigIndexing(indexingDryRun, listener); |
@@ -732,7 +807,7 @@ protected void validateAgainstExistingHCConfig(String configId, boolean indexing |
732 | 807 | ) |
733 | 808 | ); |
734 | 809 | } else { |
735 | | - validateCategoricalFieldsInAllIndices(configId, indexingDryRun, listener); |
| 810 | + validateCategoricalFieldsInAllClusters(configId, indexingDryRun, listener); |
736 | 811 | } |
737 | 812 |
|
738 | 813 | } |
@@ -794,18 +869,83 @@ protected void onSearchHCConfigResponse(SearchResponse response, String detector |
794 | 869 | } |
795 | 870 | listener.onFailure(new IllegalArgumentException(errorMsg)); |
796 | 871 | } else { |
797 | | - validateCategoricalFieldsInAllIndices(detectorId, indexingDryRun, listener); |
| 872 | + validateCategoricalFieldsInAllClusters(detectorId, indexingDryRun, listener); |
798 | 873 | } |
799 | 874 | } |
800 | 875 |
|
801 | | - protected void validateCategoricalFieldsInAllIndices(String configId, boolean indexingDryRun, ActionListener<T> listener) { |
802 | | - HashMap<String, List<String>> clusterIndicesMap = CrossClusterConfigUtils |
803 | | - .separateClusterIndexes(config.getIndices(), clusterService); |
| 876 | + protected void validateCategoricalFieldsInAllClusters(String configId, boolean indexingDryRun, ActionListener<T> listener) { |
| 877 | + List<String> wildcardClusterIndices = config.getIndices().stream().filter(idx -> idx.startsWith("*:")).toList(); |
| 878 | + List<String> nonWildcardClusterIndices = config.getIndices().stream().filter(idx -> !idx.startsWith("*:")).toList(); |
| 879 | + |
| 880 | + HashMap<String, List<String>> clusterIndicesMap = new HashMap<>(); |
| 881 | + |
| 882 | + // if no wildcard cluster indices found, fallback to typical validation |
| 883 | + if (wildcardClusterIndices.isEmpty()) { |
| 884 | + if (!nonWildcardClusterIndices.isEmpty()) { |
| 885 | + HashMap<String, List<String>> nonWildcardIndicesMap = CrossClusterConfigUtils |
| 886 | + .separateClusterIndexes(nonWildcardClusterIndices, clusterService); |
| 887 | + clusterIndicesMap.putAll(nonWildcardIndicesMap); |
| 888 | + validateCategoricalField(clusterIndicesMap.entrySet().iterator(), configId, indexingDryRun, listener); |
| 889 | + } else { |
| 890 | + listener |
| 891 | + .onFailure( |
| 892 | + new ValidationException( |
| 893 | + "No indices specified for categorical field validation.", |
| 894 | + ValidationIssueType.CATEGORY, |
| 895 | + configValidationAspect |
| 896 | + ) |
| 897 | + ); |
| 898 | + } |
| 899 | + return; |
| 900 | + } |
| 901 | + |
| 902 | + // if there are wildcard cluster indices, check for remote clusters. throw exception if none configured |
| 903 | + RemoteClusterService remoteClusterService = transportService.getRemoteClusterService(); |
| 904 | + Set<String> remoteClusters = remoteClusterService.getRegisteredRemoteClusterNames(); |
| 905 | + |
| 906 | + if (remoteClusters.isEmpty()) { |
| 907 | + listener |
| 908 | + .onFailure( |
| 909 | + new ValidationException( |
| 910 | + "Indices with wildcard cluster prefix specified, but no remote clusters are configured. Please configure remote clusters or remove the wildcard cluster prefix from the indices.", |
| 911 | + ValidationIssueType.CATEGORY, |
| 912 | + configValidationAspect |
| 913 | + ) |
| 914 | + ); |
| 915 | + return; |
| 916 | + } |
| 917 | + |
| 918 | + // for each remote cluster, add all indices with wildcard cluster prefix |
| 919 | + for (String remote : remoteClusters) { |
| 920 | + List<String> remoteIndices = wildcardClusterIndices |
| 921 | + .stream() |
| 922 | + .map(idx -> idx.substring(2)) // remove "*:" prefix |
| 923 | + .toList(); |
804 | 924 |
|
805 | | - Iterator<Map.Entry<String, List<String>>> iterator = clusterIndicesMap.entrySet().iterator(); |
| 925 | + if (!remoteIndices.isEmpty()) { |
| 926 | + clusterIndicesMap.put(remote, remoteIndices); |
| 927 | + } |
| 928 | + } |
806 | 929 |
|
807 | | - validateCategoricalField(iterator, configId, indexingDryRun, listener); |
| 930 | + // add non-wildcard cluster indices (local or explicit remote) to the map |
| 931 | + if (!nonWildcardClusterIndices.isEmpty()) { |
| 932 | + HashMap<String, List<String>> nonWildcardIndicesMap = CrossClusterConfigUtils |
| 933 | + .separateClusterIndexes(nonWildcardClusterIndices, clusterService); |
| 934 | + clusterIndicesMap.putAll(nonWildcardIndicesMap); |
| 935 | + } |
808 | 936 |
|
| 937 | + if (clusterIndicesMap.isEmpty()) { |
| 938 | + listener |
| 939 | + .onFailure( |
| 940 | + new ValidationException( |
| 941 | + "No indices found for categorical field validation.", |
| 942 | + ValidationIssueType.CATEGORY, |
| 943 | + configValidationAspect |
| 944 | + ) |
| 945 | + ); |
| 946 | + } else { |
| 947 | + validateCategoricalField(clusterIndicesMap.entrySet().iterator(), configId, indexingDryRun, listener); |
| 948 | + } |
809 | 949 | } |
810 | 950 |
|
811 | 951 | protected void validateCategoricalField( |
|
0 commit comments