Browse Source

Add granular error list to alias action response (#106514)

When an alias action list is posted with must_exist==false, and succeeds only partially, a list of results for each action are now returned. The results contain information about the requested action, indices, and aliases. If must_exist==true, or all actions fail, the call will return a 400 status along with the associated exception.
Parker Timmins 1 year ago
parent
commit
75228dfd45
26 changed files with 766 additions and 58 deletions
  1. 6 0
      docs/changelog/106514.yaml
  2. 71 0
      docs/reference/alias.asciidoc
  3. 56 2
      docs/reference/indices/aliases.asciidoc
  4. 83 0
      modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/140_data_stream_aliases.yml
  5. 97 0
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.update_aliases/40_must_exist.yml
  6. 1 0
      server/src/main/java/org/elasticsearch/TransportVersions.java
  7. 9 1
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesClusterStateUpdateRequest.java
  8. 4 1
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java
  9. 1 2
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequestBuilder.java
  10. 245 0
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesResponse.java
  11. 24 8
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/TransportIndicesAliasesAction.java
  12. 3 2
      server/src/main/java/org/elasticsearch/client/internal/IndicesAdminClient.java
  13. 3 2
      server/src/main/java/org/elasticsearch/client/internal/support/AbstractClient.java
  14. 2 2
      server/src/main/java/org/elasticsearch/cluster/metadata/AliasAction.java
  15. 9 6
      server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexAliasesService.java
  16. 108 0
      server/src/test/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesResponseTests.java
  17. 10 6
      server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexAliasesServiceTests.java
  18. 4 2
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/annotations/AnnotationIndex.java
  19. 2 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/MlIndexAndAlias.java
  20. 4 4
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStepTests.java
  21. 6 5
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/MlIndexAndAliasTests.java
  22. 6 5
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/SearchApplicationIndexService.java
  23. 3 2
      x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MlInitializationService.java
  24. 5 4
      x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleter.java
  25. 2 1
      x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java
  26. 2 2
      x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/TransformClusterStateListener.java

+ 6 - 0
docs/changelog/106514.yaml

@@ -0,0 +1,6 @@
+pr: 106514
+summary: Add granular error list to alias action response
+area: Indices APIs
+type: feature
+issues:
+ - 94478

+ 71 - 0
docs/reference/alias.asciidoc

@@ -121,6 +121,77 @@ POST _aliases
 // TEST[s/^/PUT _data_stream\/logs-nginx.access-prod\nPUT _data_stream\/logs-my_app-default\n/]
 // end::alias-multiple-actions-example[]
 
+[discrete]
+[[multiple-action-results]]
+=== Multiple action results
+
+When using multiple actions, if some succeed and some fail, a list of per-action results will be returned.
+
+Consider a similar action list to the previous example, but now with an alias `log-non-existing`, which does not yet exist.
+In this case, the `remove` action will fail, but the `add` action will succeed.
+The response will contain the list `action_results`, with a result for every requested action.
+
+[source,console]
+----
+POST _aliases
+{
+  "actions": [
+    {
+      "remove": {
+        "index": "index1",
+        "alias": "logs-non-existing"
+      }
+    },
+    {
+      "add": {
+        "index": "index2",
+        "alias": "logs-non-existing"
+      }
+    }
+  ]
+}
+----
+// TEST[s/^/PUT \/index1\nPUT \/index2\n/]
+
+The API returns the following result:
+
+[source,console-result]
+--------------------------------------------------
+{
+  "acknowledged": true,
+  "errors": true,
+  "action_results": [
+    {
+      "action": {
+        "type": "remove",
+        "indices": [ "index1" ],
+        "aliases": [ "logs-non-existing" ],
+      },
+      "status": 404,
+      "error": {
+        "type": "aliases_not_found_exception",
+        "reason": "aliases [logs-non-existing] missing",
+        "resource.type": "aliases",
+        "resource.id": "logs-non-existing"
+      }
+    },
+    {
+      "action": {
+        "type": "add",
+        "indices": [ "index2" ],
+        "aliases": [ "logs-non-existing" ],
+      },
+      "status": 200
+    }
+  ]
+}
+--------------------------------------------------
+
+Allowing the action list to succeed partially may not provide the desired result.
+It may be more appropriate to set `must_exist` to `true`, which will cause the entire action
+list to fail if a single action fails.
+
+
 [discrete]
 [[add-alias-at-creation]]
 === Add an alias at index creation

+ 56 - 2
docs/reference/indices/aliases.asciidoc

@@ -145,10 +145,16 @@ the alias points to one data stream.
 +
 Only the `add` action supports this parameter.
 
+// tag::alias-options[]
 `must_exist`::
 (Optional, Boolean)
-If `true`, the alias must exist to perform the action. Defaults to `false`. Only
-the `remove` action supports this parameter.
+Affects the behavior when attempting to remove an alias which does not exist.
+If `true`, removing an alias which does not exist will cause all actions to fail.
+If `false`, removing an alias which does not exist will only cause that removal to fail.
+Defaults to `false`.
+// end::alias-options[]
++
+Only the `remove` action supports this parameter.
 
 // tag::alias-options[]
 `routing`::
@@ -168,3 +174,51 @@ stream aliases don't support this parameter.
 Only the `add` action supports this parameter.
 =====
 ====
+
+
+
+[role="child_attributes"]
+[[indices-aliases-api-response-body]]
+==== {api-response-body-title}
+
+`acknowledged`::
+(Boolean)
+If `true`, the request received a response from the master node within the
+`timeout` period.
+
+`errors`::
+(Boolean)
+If `true`, at least one of the requested actions failed.
+
+`action_results`::
+(Optional, array of objects) Results for each requested action.
++
+.Properties of `action_results` objects
+[%collapsible%open]
+====
+
+`action`::
+(object)
+Description of the associated action request.
++
+.Properties of `action` object
+[%collapsible%open]
+=====
+`type`::
+(string) The type of the associated action, one of `add`, `remove`, or `remove_index`.
+
+`indices`::
+(array of strings) List of indices in the associated action.
+
+`aliases`::
+(array of strings) List of aliases in the associated action.
+=====
+
+`status`::
+(integer) HTTP status code returned for the action.
+
+`error`::
+(Optional, object) Contains additional information about the failed action.
++
+Only present if the action failed.
+====

+ 83 - 0
modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/140_data_stream_aliases.yml

@@ -307,3 +307,86 @@
       indices.get_alias:
         name: this-does-not-exist*
   - is_false: ds-first.aliases.my-alias
+---
+"Action Results with multiple matching aliases":
+  - skip:
+      version: " - 8.13.99"
+      reason: "alias action results do not work until 8.14"
+      features: allowed_warnings
+  - do:
+      allowed_warnings:
+        - "index template [my-template] has index patterns [log-*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-template] will take precedence during new index creation"
+      indices.put_index_template:
+        name: my-template
+        body:
+          index_patterns: [ log-* ]
+          template:
+            settings:
+              index.number_of_replicas: 0
+          data_stream: { }
+  - do:
+      indices.create_data_stream:
+        name: log-foobar
+  - is_true: acknowledged
+  - do:
+      indices.update_aliases:
+        body:
+          actions:
+            - add:
+                index: log-foobar
+                aliases: test_alias1
+            - remove:
+                index: log-foobar
+                aliases: test_non_existing
+                must_exist: false
+  - is_true: errors
+  - length: { action_results: 2 }
+  - match: { action_results.0.status: 200 }
+  - match: { action_results.0.action: { 'type': 'add', 'indices': ['log-foobar'], 'aliases': ['test_alias1'] } }
+  - match: { action_results.0.error: null }
+  - match: { action_results.1.status: 404 }
+  - match: { action_results.1.action: { 'type': 'remove', 'indices': ['log-foobar'], 'aliases': ['test_non_existing'] } }
+  - match: { action_results.1.error.type: aliases_not_found_exception }
+---
+"Single action result per action":
+  - skip:
+      version: " - 8.13.99"
+      reason: "alias action results do not work until 8.14"
+      features: allowed_warnings
+  - do:
+      allowed_warnings:
+        - "index template [my-template] has index patterns [log-*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-template] will take precedence during new index creation"
+      indices.put_index_template:
+        name: my-template
+        body:
+          index_patterns: [ log-* ]
+          template:
+            settings:
+              index.number_of_replicas: 0
+          data_stream: { }
+  - do:
+      indices.create_data_stream:
+        name: log-test-1
+  - do:
+      indices.create_data_stream:
+        name: log-test-2
+  - is_true: acknowledged
+  - do:
+      indices.update_aliases:
+        body:
+          actions:
+            - add:
+                index: log-test-*
+                aliases: test_alias1
+            - remove:
+                index: log-test-*
+                aliases: test_non_existing
+                must_exist: false
+  - is_true: errors
+  - length: { action_results: 2 }
+  - match: { action_results.0.status: 200}
+  - match: { action_results.0.action: { 'type': 'add', 'indices': ['log-test-1', 'log-test-2'], 'aliases': ['test_alias1'] } }
+  - match: { action_results.0.error: null }
+  - match: { action_results.1.status: 404 }
+  - match: { action_results.1.action: { 'type': 'remove', 'indices': ['log-test-1', 'log-test-2'], 'aliases': ['test_non_existing'] } }
+  - match: { action_results.1.error.type: aliases_not_found_exception }

+ 97 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.update_aliases/40_must_exist.yml

@@ -82,3 +82,100 @@
             - remove_index:
                 index: test_index
                 must_exist: true
+---
+"Partial success with must_exist == false":
+  - skip:
+      version: " - 8.13.99"
+      reason: "alias action results do not work until 8.14"
+  - do:
+      indices.create:
+        index: test_index
+  - do:
+      indices.update_aliases:
+        body:
+          actions:
+            - add:
+                index: test_index
+                aliases: test_alias1
+            - remove:
+                index: test_index
+                aliases: test_non_existing
+                must_exist: false
+  - is_true: errors
+  - match: { action_results.0.status: 200 }
+  - match: { action_results.0.action: { 'type': 'add', 'indices': ['test_index'], 'aliases': ['test_alias1'] } }
+  - match: { action_results.0.error: null }
+  - match: { action_results.1.status: 404 }
+  - match: { action_results.1.action: { 'type': 'remove', 'indices': ['test_index'], 'aliases': ['test_non_existing'] } }
+  - match: { action_results.1.error.type: aliases_not_found_exception }
+---
+"Partial success with must_exist == null (default)":
+  - skip:
+      version: " - 8.13.99"
+      reason: "alias action results do not work until 8.14"
+  - do:
+      indices.create:
+        index: test_index
+  - do:
+      indices.update_aliases:
+        body:
+          actions:
+            - add:
+                index: test_index
+                aliases: test_alias1
+            - remove:
+                index: test_index
+                aliases: test_non_existing
+  - is_true: errors
+  - match: { action_results.0.status: 200}
+  - match: { action_results.0.action: { 'type': 'add', 'indices': ['test_index'], 'aliases': ['test_alias1'] } }
+  - match: { action_results.0.error: null }
+  - match: { action_results.1.status: 404}
+  - match: { action_results.1.action: { 'type': 'remove', 'indices': ['test_index'], 'aliases': ['test_non_existing'] } }
+  - match: { action_results.1.error.type: aliases_not_found_exception }
+---
+"No action_results field if all actions successful":
+  - skip:
+      version: " - 8.13.99"
+      reason: "alias action results do not work until 8.14"
+  - do:
+      indices.create:
+        index: test_index
+  - do:
+     indices.update_aliases:
+      body:
+        actions:
+          - add:
+              index: test_index
+              aliases: test_alias1
+  - is_false: errors
+  - match: { action_results: null }
+---
+"Single result per input action":
+  - skip:
+      version: " - 8.13.99"
+      reason: "alias action results do not work until 8.14"
+  - do:
+      indices.create:
+        index: test_index1
+  - do:
+      indices.create:
+        index: test_index2
+  - do:
+      indices.update_aliases:
+        body:
+          actions:
+            - add:
+                index: test_index*
+                aliases: test_alias1
+            - remove:
+                index: test_index*
+                aliases: test_non_existing
+  - length: { action_results: 2 }
+  - is_true: errors
+  - match: { action_results.0.status: 200}
+  - match: { action_results.0.action: { 'type': 'add', 'indices': ['test_index1', 'test_index2'], 'aliases': ['test_alias1'] } }
+  - match: { action_results.0.error: null }
+  - match: { action_results.1.status: 404}
+  - match: { action_results.1.action: { 'type': 'remove', 'indices': ['test_index1', 'test_index2'], 'aliases': ['test_non_existing'] } }
+  - match: { action_results.1.error.type: aliases_not_found_exception }

+ 1 - 0
server/src/main/java/org/elasticsearch/TransportVersions.java

@@ -164,6 +164,7 @@ public class TransportVersions {
     public static final TransportVersion ESQL_ORDINAL_BLOCK = def(8_623_00_0);
     public static final TransportVersion ML_INFERENCE_COHERE_RERANK = def(8_624_00_0);
     public static final TransportVersion INDEXING_PRESSURE_DOCUMENT_REJECTIONS_COUNT = def(8_625_00_0);
+    public static final TransportVersion ALIAS_ACTION_RESULTS = def(8_626_00_0);
 
     /*
      * STOP! READ THIS FIRST! No, really,

+ 9 - 1
server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesClusterStateUpdateRequest.java

@@ -7,6 +7,7 @@
  */
 package org.elasticsearch.action.admin.indices.alias;
 
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse.AliasActionResult;
 import org.elasticsearch.cluster.ack.ClusterStateUpdateRequest;
 import org.elasticsearch.cluster.metadata.AliasAction;
 
@@ -18,8 +19,11 @@ import java.util.List;
 public class IndicesAliasesClusterStateUpdateRequest extends ClusterStateUpdateRequest<IndicesAliasesClusterStateUpdateRequest> {
     private final List<AliasAction> actions;
 
-    public IndicesAliasesClusterStateUpdateRequest(List<AliasAction> actions) {
+    private final List<IndicesAliasesResponse.AliasActionResult> actionResults;
+
+    public IndicesAliasesClusterStateUpdateRequest(List<AliasAction> actions, List<AliasActionResult> actionResults) {
         this.actions = actions;
+        this.actionResults = actionResults;
     }
 
     /**
@@ -28,4 +32,8 @@ public class IndicesAliasesClusterStateUpdateRequest extends ClusterStateUpdateR
     public List<AliasAction> actions() {
         return actions;
     }
+
+    public List<AliasActionResult> getActionResults() {
+        return actionResults;
+    }
 }

+ 4 - 1
server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java

@@ -83,7 +83,6 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
         private static final ParseField IS_WRITE_INDEX = new ParseField("is_write_index");
         private static final ParseField IS_HIDDEN = new ParseField("is_hidden");
         private static final ParseField MUST_EXIST = new ParseField("must_exist");
-
         private static final ParseField ADD = new ParseField("add");
         private static final ParseField REMOVE = new ParseField("remove");
         private static final ParseField REMOVE_INDEX = new ParseField("remove_index");
@@ -105,6 +104,10 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
                 return value;
             }
 
+            public String getFieldName() {
+                return fieldName;
+            }
+
             public static Type fromValue(byte value) {
                 return switch (value) {
                     case 0 -> ADD;

+ 1 - 2
server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequestBuilder.java

@@ -10,7 +10,6 @@ package org.elasticsearch.action.admin.indices.alias;
 
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions;
 import org.elasticsearch.action.support.master.AcknowledgedRequestBuilder;
-import org.elasticsearch.action.support.master.AcknowledgedResponse;
 import org.elasticsearch.client.internal.ElasticsearchClient;
 import org.elasticsearch.index.query.QueryBuilder;
 
@@ -21,7 +20,7 @@ import java.util.Map;
  */
 public class IndicesAliasesRequestBuilder extends AcknowledgedRequestBuilder<
     IndicesAliasesRequest,
-    AcknowledgedResponse,
+    IndicesAliasesResponse,
     IndicesAliasesRequestBuilder> {
 
     public IndicesAliasesRequestBuilder(ElasticsearchClient client) {

+ 245 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesResponse.java

@@ -0,0 +1,245 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.action.admin.indices.alias;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.TransportVersions;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.rest.action.admin.indices.AliasesNotFoundException;
+import org.elasticsearch.xcontent.ToXContentObject;
+import org.elasticsearch.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Response with error information for a request to add/remove aliases for one or more indices.
+ * Contains an acknowledged boolean, an errors boolean, and a list of results.
+ * The result list is only present if there are errors, and contains a result for every input action.
+ * This response replaces AcknowledgedResponse, and knows how to de/serialize from/to AcknowledgedResponse
+ * in case of mixed version clusters.
+ */
+public class IndicesAliasesResponse extends AcknowledgedResponse {
+
+    // Response without any error information, analogous to AcknowledgedResponse.FALSE
+    public static final IndicesAliasesResponse NOT_ACKNOWLEDGED = new IndicesAliasesResponse(false, false, List.of());
+
+    // Response without any error information, analogous to AcknowledgedResponse.TRUE
+    public static final IndicesAliasesResponse ACKNOWLEDGED_NO_ERRORS = new IndicesAliasesResponse(true, false, List.of());
+
+    private static final String ACTION_RESULTS_FIELD = "action_results";
+    private static final String ERRORS_FIELD = "errors";
+
+    private final List<AliasActionResult> actionResults;
+    private final boolean errors;
+
+    protected IndicesAliasesResponse(StreamInput in) throws IOException {
+        super(in);
+
+        if (in.getTransportVersion().onOrAfter(TransportVersions.ALIAS_ACTION_RESULTS)) {
+            this.errors = in.readBoolean();
+            this.actionResults = in.readCollectionAsImmutableList(AliasActionResult::new);
+        } else {
+            this.errors = false;
+            this.actionResults = List.of();
+        }
+    }
+
+    /**
+     * @param acknowledged  whether the update was acknowledged by all the relevant nodes in the cluster
+     * @param errors true if any of the requested actions failed
+     * @param actionResults the list of results for each input action, only present if there are errors
+     */
+    IndicesAliasesResponse(boolean acknowledged, boolean errors, final List<AliasActionResult> actionResults) {
+        super(acknowledged);
+        this.errors = errors;
+        this.actionResults = actionResults;
+    }
+
+    public List<AliasActionResult> getActionResults() {
+        return actionResults;
+    }
+
+    public boolean hasErrors() {
+        return errors;
+    }
+
+    /**
+     *  Build a response from a list of action results. Sets the errors boolean based
+     *  on whether an of the individual results contain an error.
+     * @param actionResults an action result for each of the requested alias actions
+     * @return response containing all action results
+     */
+    public static IndicesAliasesResponse build(final List<AliasActionResult> actionResults) {
+        assert actionResults.isEmpty() == false : "IndicesAliasesResponse must be instantiated with at least one action result.";
+        final boolean errors = actionResults.stream().anyMatch(a -> a.error != null);
+        return new IndicesAliasesResponse(true, errors, actionResults);
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        super.writeTo(out);
+        if (out.getTransportVersion().onOrAfter(TransportVersions.ALIAS_ACTION_RESULTS)) {
+            out.writeBoolean(errors);
+            out.writeCollection(actionResults);
+        }
+    }
+
+    @Override
+    protected void addCustomFields(XContentBuilder builder, Params params) throws IOException {
+        builder.field(ERRORS_FIELD, errors);
+        // if there are no errors, don't provide granular list of results
+        if (errors) {
+            builder.field(ACTION_RESULTS_FIELD, actionResults);
+        }
+    }
+
+    @Override
+    // Only used equals in tests
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        if (super.equals(o) == false) return false;
+        IndicesAliasesResponse response = (IndicesAliasesResponse) o;
+        return errors == response.errors && Objects.equals(actionResults, response.actionResults);
+    }
+
+    @Override
+    // Only used hashCode in tests
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), actionResults, errors);
+    }
+
+    /**
+     * Result for a single alias add/remove action
+     */
+    public static class AliasActionResult implements Writeable, ToXContentObject {
+
+        /**
+         * Resolved indices to which the action applies. This duplicates information
+         * which exists in the action, but is included because the action indices may
+         * or may not be resolved depending on if the security layer is used or not.
+         */
+        private final List<String> indices;
+        private final AliasActions action;
+        private final ElasticsearchException error;
+
+        /**
+         * Build result that could be either a success or failure
+         * @param indices the resolved indices to which the associated action applies
+         * @param action the alias action consisting of add/remove, aliases, and indices
+         * @param numAliasesRemoved the number of aliases remove, if any
+         * @return the action result
+         */
+        public static AliasActionResult build(List<String> indices, AliasActions action, int numAliasesRemoved) {
+            if (action.actionType() == AliasActions.Type.REMOVE && numAliasesRemoved == 0) {
+                return buildRemoveError(indices, action);
+            }
+            return buildSuccess(indices, action);
+        }
+
+        /**
+         * Build an error result for a failed remove action.
+         */
+        private static AliasActionResult buildRemoveError(List<String> indices, AliasActions action) {
+            return new AliasActionResult(indices, action, new AliasesNotFoundException((action.getOriginalAliases())));
+        }
+
+        /**
+         * Build a success action result with no errors.
+         */
+        public static AliasActionResult buildSuccess(List<String> indices, AliasActions action) {
+            return new AliasActionResult(indices, action, null);
+        }
+
+        private int getStatus() {
+            return error == null ? 200 : error.status().getStatus();
+        }
+
+        private AliasActionResult(List<String> indices, AliasActions action, ElasticsearchException error) {
+            assert indices.isEmpty() == false : "Alias action result must be instantiated with at least one index";
+            this.indices = indices;
+            this.action = action;
+            this.error = error;
+        }
+
+        private AliasActionResult(StreamInput in) throws IOException {
+            this.indices = in.readStringCollectionAsList();
+            this.action = new AliasActions(in);
+            this.error = in.readException();
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            out.writeStringCollection(indices);
+            action.writeTo(out);
+            out.writeException(error);
+        }
+
+        public static final String ACTION_FIELD = "action";
+        public static final String ACTION_TYPE_FIELD = "type";
+        public static final String ACTION_INDICES_FIELD = "indices";
+        public static final String ACTION_ALIASES_FIELD = "aliases";
+        public static final String STATUS_FIELD = "status";
+        public static final String ERROR_FIELD = "error";
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+
+            // include subset of fields from action request
+            builder.field(ACTION_FIELD);
+            builder.startObject();
+            builder.field(ACTION_TYPE_FIELD, action.actionType().getFieldName());
+            builder.field(ACTION_INDICES_FIELD, indices.stream().sorted().collect(Collectors.toList()));
+            builder.array(ACTION_ALIASES_FIELD, action.getOriginalAliases());
+            builder.endObject();
+
+            builder.field(STATUS_FIELD, getStatus());
+
+            if (error != null) {
+                builder.startObject(ERROR_FIELD);
+                error.toXContent(builder, params);
+                builder.endObject();
+            }
+            builder.endObject();
+            return builder;
+        }
+
+        @Override
+        // Only used equals in tests
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            AliasActionResult that = (AliasActionResult) o;
+            return Objects.equals(indices, that.indices) && Objects.equals(action, that.action)
+            // ElasticsearchException does not have hashCode() so assume errors are equal iff class and message are equal
+                && Objects.equals(error == null ? null : error.getMessage(), that.error == null ? null : that.error.getMessage())
+                && Objects.equals(error == null ? null : error.getClass(), that.error == null ? null : that.error.getClass());
+        }
+
+        @Override
+        // Only used hashCode in tests
+        public int hashCode() {
+            return Objects.hash(
+                indices,
+                action,
+                // ElasticsearchException does not have hashCode() so assume errors are equal iff class and message are equal
+                error == null ? null : error.getMessage(),
+                error == null ? null : error.getClass()
+            );
+        }
+    }
+}

+ 24 - 8
server/src/main/java/org/elasticsearch/action/admin/indices/alias/TransportIndicesAliasesAction.java

@@ -14,9 +14,9 @@ import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.ActionType;
 import org.elasticsearch.action.RequestValidators;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse.AliasActionResult;
 import org.elasticsearch.action.support.ActionFilters;
-import org.elasticsearch.action.support.master.AcknowledgedResponse;
-import org.elasticsearch.action.support.master.AcknowledgedTransportMasterNodeAction;
+import org.elasticsearch.action.support.master.TransportMasterNodeAction;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.block.ClusterBlockException;
 import org.elasticsearch.cluster.block.ClusterBlockLevel;
@@ -56,10 +56,10 @@ import static java.util.Collections.unmodifiableList;
 /**
  * Add/remove aliases action
  */
-public class TransportIndicesAliasesAction extends AcknowledgedTransportMasterNodeAction<IndicesAliasesRequest> {
+public class TransportIndicesAliasesAction extends TransportMasterNodeAction<IndicesAliasesRequest, IndicesAliasesResponse> {
 
     public static final String NAME = "indices:admin/aliases";
-    public static final ActionType<AcknowledgedResponse> TYPE = new ActionType<>(NAME);
+    public static final ActionType<IndicesAliasesResponse> TYPE = new ActionType<>(NAME);
     private static final Logger logger = LogManager.getLogger(TransportIndicesAliasesAction.class);
 
     private final MetadataIndexAliasesService indexAliasesService;
@@ -85,6 +85,7 @@ public class TransportIndicesAliasesAction extends AcknowledgedTransportMasterNo
             actionFilters,
             IndicesAliasesRequest::new,
             indexNameExpressionResolver,
+            IndicesAliasesResponse::new,
             EsExecutors.DIRECT_EXECUTOR_SERVICE
         );
         this.indexAliasesService = indexAliasesService;
@@ -106,15 +107,19 @@ public class TransportIndicesAliasesAction extends AcknowledgedTransportMasterNo
         Task task,
         final IndicesAliasesRequest request,
         final ClusterState state,
-        final ActionListener<AcknowledgedResponse> listener
+        final ActionListener<IndicesAliasesResponse> listener
     ) {
 
         // Expand the indices names
         List<AliasActions> actions = request.aliasActions();
         List<AliasAction> finalActions = new ArrayList<>();
+        List<AliasActionResult> actionResults = new ArrayList<>();
         // Resolve all the AliasActions into AliasAction instances and gather all the aliases
         Set<String> aliases = new HashSet<>();
         for (AliasActions action : actions) {
+            int numAliasesRemoved = 0;
+            List<String> resolvedIndices = new ArrayList<>();
+
             List<String> concreteDataStreams = indexNameExpressionResolver.dataStreamNames(
                 state,
                 request.indicesOptions(),
@@ -161,18 +166,24 @@ public class TransportIndicesAliasesAction extends AcknowledgedTransportMasterNo
                                 finalActions.add(new AddDataStreamAlias(alias, dataStreamName, action.writeIndex(), action.filter()));
                             }
                         }
+
+                        actionResults.add(AliasActionResult.buildSuccess(concreteDataStreams, action));
                         continue;
                     }
                     case REMOVE -> {
                         for (String dataStreamName : concreteDataStreams) {
                             for (String alias : concreteDataStreamAliases(action, state.metadata(), dataStreamName)) {
                                 finalActions.add(new AliasAction.RemoveDataStreamAlias(alias, dataStreamName, action.mustExist()));
+                                numAliasesRemoved++;
                             }
                         }
+
                         if (nonBackingIndices.isEmpty() == false) {
                             // Regular aliases/indices match as well with the provided expression.
                             // (Only when adding new aliases, matching both data streams and indices is disallowed)
+                            resolvedIndices.addAll(concreteDataStreams);
                         } else {
+                            actionResults.add(AliasActionResult.build(concreteDataStreams, action, numAliasesRemoved));
                             continue;
                         }
                     }
@@ -224,6 +235,7 @@ public class TransportIndicesAliasesAction extends AcknowledgedTransportMasterNo
                     case REMOVE:
                         for (String alias : concreteAliases(action, state.metadata(), index.getName())) {
                             finalActions.add(new AliasAction.Remove(index.getName(), alias, action.mustExist()));
+                            numAliasesRemoved++;
                         }
                         break;
                     case REMOVE_INDEX:
@@ -233,14 +245,18 @@ public class TransportIndicesAliasesAction extends AcknowledgedTransportMasterNo
                         throw new IllegalArgumentException("Unsupported action [" + action.actionType() + "]");
                 }
             }
+
+            Arrays.stream(concreteIndices).map(Index::getName).forEach(resolvedIndices::add);
+            actionResults.add(AliasActionResult.build(resolvedIndices, action, numAliasesRemoved));
         }
         if (finalActions.isEmpty() && false == actions.isEmpty()) {
             throw new AliasesNotFoundException(aliases.toArray(new String[aliases.size()]));
         }
         request.aliasActions().clear();
-        IndicesAliasesClusterStateUpdateRequest updateRequest = new IndicesAliasesClusterStateUpdateRequest(unmodifiableList(finalActions))
-            .ackTimeout(request.timeout())
-            .masterNodeTimeout(request.masterNodeTimeout());
+        IndicesAliasesClusterStateUpdateRequest updateRequest = new IndicesAliasesClusterStateUpdateRequest(
+            unmodifiableList(finalActions),
+            unmodifiableList(actionResults)
+        ).ackTimeout(request.timeout()).masterNodeTimeout(request.masterNodeTimeout());
 
         indexAliasesService.indicesAliases(updateRequest, listener.delegateResponse((l, e) -> {
             logger.debug("failed to perform aliases", e);

+ 3 - 2
server/src/main/java/org/elasticsearch/client/internal/IndicesAdminClient.java

@@ -12,6 +12,7 @@ import org.elasticsearch.action.ActionFuture;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequestBuilder;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesResponse;
@@ -371,7 +372,7 @@ public interface IndicesAdminClient extends ElasticsearchClient {
      * @param request The index aliases request
      * @return The result future
      */
-    ActionFuture<AcknowledgedResponse> aliases(IndicesAliasesRequest request);
+    ActionFuture<IndicesAliasesResponse> aliases(IndicesAliasesRequest request);
 
     /**
      * Allows to add/remove aliases from indices.
@@ -379,7 +380,7 @@ public interface IndicesAdminClient extends ElasticsearchClient {
      * @param request  The index aliases request
      * @param listener A listener to be notified with a result
      */
-    void aliases(IndicesAliasesRequest request, ActionListener<AcknowledgedResponse> listener);
+    void aliases(IndicesAliasesRequest request, ActionListener<IndicesAliasesResponse> listener);
 
     /**
      * Allows to add/remove aliases from indices.

+ 3 - 2
server/src/main/java/org/elasticsearch/client/internal/support/AbstractClient.java

@@ -118,6 +118,7 @@ import org.elasticsearch.action.admin.cluster.storedscripts.TransportDeleteStore
 import org.elasticsearch.action.admin.cluster.storedscripts.TransportPutStoredScriptAction;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.action.admin.indices.alias.TransportIndicesAliasesAction;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesAction;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest;
@@ -1083,12 +1084,12 @@ public abstract class AbstractClient implements Client {
         }
 
         @Override
-        public ActionFuture<AcknowledgedResponse> aliases(final IndicesAliasesRequest request) {
+        public ActionFuture<IndicesAliasesResponse> aliases(final IndicesAliasesRequest request) {
             return execute(TransportIndicesAliasesAction.TYPE, request);
         }
 
         @Override
-        public void aliases(final IndicesAliasesRequest request, final ActionListener<AcknowledgedResponse> listener) {
+        public void aliases(final IndicesAliasesRequest request, final ActionListener<IndicesAliasesResponse> listener) {
             execute(TransportIndicesAliasesAction.TYPE, request, listener);
         }
 

+ 2 - 2
server/src/main/java/org/elasticsearch/cluster/metadata/AliasAction.java

@@ -8,10 +8,10 @@
 
 package org.elasticsearch.cluster.metadata;
 
-import org.elasticsearch.ResourceNotFoundException;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.core.Nullable;
+import org.elasticsearch.rest.action.admin.indices.AliasesNotFoundException;
 
 /**
  * Individual operation to perform on the cluster state as part of an {@link IndicesAliasesRequest}.
@@ -189,7 +189,7 @@ public abstract class AliasAction {
         boolean apply(NewAliasValidator aliasValidator, Metadata.Builder metadata, IndexMetadata index) {
             if (false == index.getAliases().containsKey(alias)) {
                 if (mustExist != null && mustExist) {
-                    throw new ResourceNotFoundException("required alias [" + alias + "] does not exist");
+                    throw new AliasesNotFoundException(alias);
                 }
                 return false;
             }

+ 9 - 6
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexAliasesService.java

@@ -11,7 +11,7 @@ package org.elasticsearch.cluster.metadata;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesClusterStateUpdateRequest;
-import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.ClusterStateAckListener;
 import org.elasticsearch.cluster.ClusterStateTaskExecutor;
@@ -79,7 +79,10 @@ public class MetadataIndexAliasesService {
         this.taskQueue = clusterService.createTaskQueue("index-aliases", Priority.URGENT, this.executor);
     }
 
-    public void indicesAliases(final IndicesAliasesClusterStateUpdateRequest request, final ActionListener<AcknowledgedResponse> listener) {
+    public void indicesAliases(
+        final IndicesAliasesClusterStateUpdateRequest request,
+        final ActionListener<IndicesAliasesResponse> listener
+    ) {
         taskQueue.submitTask("index-aliases", new ApplyAliasesTask(request, listener), null); // TODO use request.masterNodeTimeout() here?
     }
 
@@ -254,7 +257,7 @@ public class MetadataIndexAliasesService {
     /**
      * A cluster state update task that consists of the cluster state request and the listeners that need to be notified upon completion.
      */
-    record ApplyAliasesTask(IndicesAliasesClusterStateUpdateRequest request, ActionListener<AcknowledgedResponse> listener)
+    record ApplyAliasesTask(IndicesAliasesClusterStateUpdateRequest request, ActionListener<IndicesAliasesResponse> listener)
         implements
             ClusterStateTaskListener,
             ClusterStateAckListener {
@@ -271,17 +274,17 @@ public class MetadataIndexAliasesService {
 
         @Override
         public void onAllNodesAcked() {
-            listener.onResponse(AcknowledgedResponse.TRUE);
+            listener.onResponse(IndicesAliasesResponse.build(request.getActionResults()));
         }
 
         @Override
         public void onAckFailure(Exception e) {
-            listener.onResponse(AcknowledgedResponse.FALSE);
+            listener.onResponse(IndicesAliasesResponse.NOT_ACKNOWLEDGED);
         }
 
         @Override
         public void onAckTimeout() {
-            listener.onResponse(AcknowledgedResponse.FALSE);
+            listener.onResponse(IndicesAliasesResponse.NOT_ACKNOWLEDGED);
         }
 
         @Override

+ 108 - 0
server/src/test/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesResponseTests.java

@@ -0,0 +1,108 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.action.admin.indices.alias;
+
+import org.elasticsearch.TransportVersions;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.index.alias.RandomAliasActionsGenerator;
+import org.elasticsearch.test.AbstractWireSerializingTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class IndicesAliasesResponseTests extends AbstractWireSerializingTestCase<IndicesAliasesResponse> {
+    public void testMixedModeSerialization() throws IOException {
+
+        // AcknowledgedResponse to IndicesAliasesResponse
+        // in version before TransportVersions.ALIAS_ACTION_RESULTS
+        {
+            var ack = AcknowledgedResponse.of(randomBoolean());
+            try (BytesStreamOutput output = new BytesStreamOutput()) {
+                ack.writeTo(output);
+                try (StreamInput in = output.bytes().streamInput()) {
+                    in.setTransportVersion(TransportVersions.V_8_12_0);
+
+                    var indicesAliasesResponse = new IndicesAliasesResponse(in);
+
+                    assertEquals(ack.isAcknowledged(), indicesAliasesResponse.isAcknowledged());
+                    assertTrue(indicesAliasesResponse.getActionResults().isEmpty());
+                    assertFalse(indicesAliasesResponse.hasErrors());
+                }
+            }
+        }
+
+        // IndicesAliasesResponse to AcknowledgedResponse
+        // out version before TransportVersions.ALIAS_ACTION_RESULTS
+        {
+            var indicesAliasesResponse = randomIndicesAliasesResponse();
+            try (BytesStreamOutput output = new BytesStreamOutput()) {
+                output.setTransportVersion(TransportVersions.V_8_12_0);
+
+                indicesAliasesResponse.writeTo(output);
+                try (StreamInput in = output.bytes().streamInput()) {
+                    var ack = AcknowledgedResponse.readFrom(in);
+                    assertEquals(ack.isAcknowledged(), indicesAliasesResponse.isAcknowledged());
+                }
+            }
+        }
+    }
+
+    @Override
+    protected Writeable.Reader<IndicesAliasesResponse> instanceReader() {
+        return IndicesAliasesResponse::new;
+    }
+
+    @Override
+    protected IndicesAliasesResponse createTestInstance() {
+        return randomIndicesAliasesResponse();
+    }
+
+    private static IndicesAliasesResponse randomIndicesAliasesResponse() {
+        int numActions = between(0, 5);
+        List<IndicesAliasesResponse.AliasActionResult> results = new ArrayList<>();
+        for (int i = 0; i < numActions; ++i) {
+            results.add(randomIndicesAliasesResult());
+        }
+        return new IndicesAliasesResponse(randomBoolean(), randomBoolean(), results);
+    }
+
+    @Override
+    protected IndicesAliasesResponse mutateInstance(IndicesAliasesResponse instance) throws IOException {
+        switch (between(0, 2)) {
+            case 0: {
+                boolean acknowledged = instance.isAcknowledged() == false;
+                return new IndicesAliasesResponse(acknowledged, instance.hasErrors(), instance.getActionResults());
+            }
+            case 1: {
+                boolean errors = instance.hasErrors() == false;
+                return new IndicesAliasesResponse(instance.isAcknowledged(), errors, instance.getActionResults());
+            }
+            default: {
+                var results = new ArrayList<>(instance.getActionResults());
+                if (results.isEmpty()) {
+                    results.add(randomIndicesAliasesResult());
+                } else {
+                    results.remove(between(0, results.size() - 1));
+                }
+                return new IndicesAliasesResponse(instance.isAcknowledged(), instance.hasErrors(), results);
+            }
+        }
+    }
+
+    private static IndicesAliasesResponse.AliasActionResult randomIndicesAliasesResult() {
+        var action = RandomAliasActionsGenerator.randomAliasAction();
+        var indices = Arrays.asList(generateRandomStringArray(10, 5, false, false));
+        return IndicesAliasesResponse.AliasActionResult.build(indices, action, randomIntBetween(0, 3));
+    }
+}

+ 10 - 6
server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexAliasesServiceTests.java

@@ -8,8 +8,9 @@
 
 package org.elasticsearch.cluster.metadata;
 
-import org.elasticsearch.ResourceNotFoundException;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesClusterStateUpdateRequest;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse.AliasActionResult;
 import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.service.ClusterService;
@@ -19,6 +20,7 @@ import org.elasticsearch.common.util.set.Sets;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.index.IndexNotFoundException;
 import org.elasticsearch.index.IndexVersion;
+import org.elasticsearch.rest.action.admin.indices.AliasesNotFoundException;
 import org.elasticsearch.test.ClusterServiceUtils;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.index.IndexVersionUtils;
@@ -156,11 +158,11 @@ public class MetadataIndexAliasesServiceTests extends ESTestCase {
 
         // Show that removing non-existing alias with mustExist == true fails
         final ClusterState finalCS = after;
-        final ResourceNotFoundException iae = expectThrows(
-            ResourceNotFoundException.class,
+        final AliasesNotFoundException iae = expectThrows(
+            AliasesNotFoundException.class,
             () -> service.applyAliasActions(finalCS, singletonList(new AliasAction.Remove(index, "test_2", true)))
         );
-        assertThat(iae.getMessage(), containsString("required alias [test_2] does not exist"));
+        assertThat(iae.getMessage(), containsString("aliases [test_2] missing"));
     }
 
     public void testMultipleIndices() {
@@ -690,10 +692,12 @@ public class MetadataIndexAliasesServiceTests extends ESTestCase {
         String index = randomAlphaOfLength(5);
         ClusterState before = createIndex(ClusterState.builder(ClusterName.DEFAULT).build(), index);
         IndicesAliasesClusterStateUpdateRequest addAliasRequest = new IndicesAliasesClusterStateUpdateRequest(
-            List.of(new AliasAction.Add(index, "test", null, null, null, null, null))
+            List.of(new AliasAction.Add(index, "test", null, null, null, null, null)),
+            List.of(AliasActionResult.buildSuccess(List.of(index), AliasActions.add().aliases("test").indices(index)))
         );
         IndicesAliasesClusterStateUpdateRequest removeAliasRequest = new IndicesAliasesClusterStateUpdateRequest(
-            List.of(new AliasAction.Remove(index, "test", true))
+            List.of(new AliasAction.Remove(index, "test", true)),
+            List.of(AliasActionResult.buildSuccess(List.of(index), AliasActions.remove().aliases("test").indices(index)))
         );
 
         ClusterState after = ClusterStateTaskExecutorUtils.executeAndAssertSuccessful(

+ 4 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/annotations/AnnotationIndex.java

@@ -14,9 +14,9 @@ import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
 import org.elasticsearch.action.admin.cluster.health.TransportClusterHealthAction;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
 import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
-import org.elasticsearch.action.support.master.AcknowledgedResponse;
 import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.IndexAbstraction;
@@ -130,7 +130,9 @@ public class AnnotationIndex {
                 client.threadPool().getThreadContext(),
                 ML_ORIGIN,
                 requestBuilder.request(),
-                finalDelegate.<AcknowledgedResponse>delegateFailureAndWrap((l, r) -> checkMappingsListener.onResponse(r.isAcknowledged())),
+                finalDelegate.<IndicesAliasesResponse>delegateFailureAndWrap(
+                    (l, r) -> checkMappingsListener.onResponse(r.isAcknowledged())
+                ),
                 client.admin().indices()::aliases
             );
         });

+ 2 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/MlIndexAndAlias.java

@@ -16,6 +16,7 @@ import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
 import org.elasticsearch.action.admin.indices.alias.Alias;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
 import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
@@ -295,7 +296,7 @@ public final class MlIndexAndAlias {
             client.threadPool().getThreadContext(),
             ML_ORIGIN,
             request,
-            listener.<AcknowledgedResponse>delegateFailureAndWrap((l, resp) -> l.onResponse(resp.isAcknowledged())),
+            listener.<IndicesAliasesResponse>delegateFailureAndWrap((l, resp) -> l.onResponse(resp.isAcknowledged())),
             client.admin().indices()::aliases
         );
     }

+ 4 - 4
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStepTests.java

@@ -9,8 +9,8 @@ package org.elasticsearch.xpack.core.ilm;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.action.support.PlainActionFuture;
-import org.elasticsearch.action.support.master.AcknowledgedResponse;
 import org.elasticsearch.cluster.metadata.AliasMetadata;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.index.IndexVersion;
@@ -90,8 +90,8 @@ public class ShrinkSetAliasStepTests extends AbstractStepTestCase<ShrinkSetAlias
             IndicesAliasesRequest request = (IndicesAliasesRequest) invocation.getArguments()[0];
             assertThat(request.getAliasActions(), equalTo(expectedAliasActions));
             @SuppressWarnings("unchecked")
-            ActionListener<AcknowledgedResponse> listener = (ActionListener<AcknowledgedResponse>) invocation.getArguments()[1];
-            listener.onResponse(AcknowledgedResponse.TRUE);
+            ActionListener<IndicesAliasesResponse> listener = (ActionListener<IndicesAliasesResponse>) invocation.getArguments()[1];
+            listener.onResponse(IndicesAliasesResponse.ACKNOWLEDGED_NO_ERRORS);
             return null;
         }).when(indicesClient).aliases(Mockito.any(), Mockito.any());
 
@@ -113,7 +113,7 @@ public class ShrinkSetAliasStepTests extends AbstractStepTestCase<ShrinkSetAlias
 
         Mockito.doAnswer((Answer<Void>) invocation -> {
             @SuppressWarnings("unchecked")
-            ActionListener<AcknowledgedResponse> listener = (ActionListener<AcknowledgedResponse>) invocation.getArguments()[1];
+            ActionListener<IndicesAliasesResponse> listener = (ActionListener<IndicesAliasesResponse>) invocation.getArguments()[1];
             listener.onFailure(exception);
             return null;
         }).when(indicesClient).aliases(Mockito.any(), Mockito.any());

+ 6 - 5
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/MlIndexAndAliasTests.java

@@ -13,11 +13,11 @@ import org.elasticsearch.action.admin.indices.alias.Alias;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
 import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
 import org.elasticsearch.action.admin.indices.template.put.TransportPutComposableIndexTemplateAction;
-import org.elasticsearch.action.support.master.AcknowledgedResponse;
 import org.elasticsearch.action.support.master.MasterNodeRequest;
 import org.elasticsearch.client.internal.AdminClient;
 import org.elasticsearch.client.internal.Client;
@@ -97,8 +97,8 @@ public class MlIndexAndAliasTests extends ESTestCase {
         );
         doAnswer(withResponse(new CreateIndexResponse(true, true, FIRST_CONCRETE_INDEX))).when(indicesAdminClient).create(any(), any());
         when(indicesAdminClient.prepareAliases()).thenReturn(new IndicesAliasesRequestBuilder(client));
-        doAnswer(withResponse(AcknowledgedResponse.TRUE)).when(indicesAdminClient).aliases(any(), any());
-        doAnswer(withResponse(AcknowledgedResponse.TRUE)).when(indicesAdminClient).putTemplate(any(), any());
+        doAnswer(withResponse(IndicesAliasesResponse.ACKNOWLEDGED_NO_ERRORS)).when(indicesAdminClient).aliases(any(), any());
+        doAnswer(withResponse(IndicesAliasesResponse.ACKNOWLEDGED_NO_ERRORS)).when(indicesAdminClient).putTemplate(any(), any());
 
         clusterAdminClient = mock(ClusterAdminClient.class);
         doAnswer(invocationOnMock -> {
@@ -116,8 +116,9 @@ public class MlIndexAndAliasTests extends ESTestCase {
         when(client.threadPool()).thenReturn(threadPool);
         when(client.admin()).thenReturn(adminClient);
         doAnswer(invocationOnMock -> {
-            ActionListener<AcknowledgedResponse> actionListener = (ActionListener<AcknowledgedResponse>) invocationOnMock.getArguments()[2];
-            actionListener.onResponse(AcknowledgedResponse.TRUE);
+            ActionListener<IndicesAliasesResponse> actionListener = (ActionListener<IndicesAliasesResponse>) invocationOnMock
+                .getArguments()[2];
+            actionListener.onResponse(IndicesAliasesResponse.ACKNOWLEDGED_NO_ERRORS);
             return null;
         }).when(client)
             .execute(

+ 6 - 5
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/SearchApplicationIndexService.java

@@ -18,6 +18,7 @@ import org.elasticsearch.action.DocWriteRequest;
 import org.elasticsearch.action.DocWriteResponse;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesResponse;
 import org.elasticsearch.action.delete.DeleteRequest;
@@ -223,7 +224,7 @@ public class SearchApplicationIndexService {
     public void putSearchApplication(SearchApplication app, boolean create, ActionListener<DocWriteResponse> listener) {
         createOrUpdateAlias(app, new ActionListener<>() {
             @Override
-            public void onResponse(AcknowledgedResponse acknowledgedResponse) {
+            public void onResponse(IndicesAliasesResponse response) {
                 updateSearchApplication(app, create, listener);
             }
 
@@ -240,7 +241,7 @@ public class SearchApplicationIndexService {
         });
     }
 
-    private void createOrUpdateAlias(SearchApplication app, ActionListener<AcknowledgedResponse> listener) {
+    private void createOrUpdateAlias(SearchApplication app, ActionListener<IndicesAliasesResponse> listener) {
 
         final Metadata metadata = clusterService.state().metadata();
         final String searchAliasName = getSearchAliasName(app);
@@ -332,14 +333,14 @@ public class SearchApplicationIndexService {
         );
         client.admin().indices().aliases(aliasesRequest, new ActionListener<>() {
             @Override
-            public void onResponse(AcknowledgedResponse acknowledgedResponse) {
-                listener.onResponse(AcknowledgedResponse.TRUE);
+            public void onResponse(IndicesAliasesResponse response) {
+                listener.onResponse(response);
             }
 
             @Override
             public void onFailure(Exception e) {
                 if (e instanceof ResourceNotFoundException) {
-                    listener.onResponse(AcknowledgedResponse.TRUE);
+                    listener.onResponse(IndicesAliasesResponse.ACKNOWLEDGED_NO_ERRORS);
                 } else {
                     listener.onFailure(e);
                 }

+ 3 - 2
x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MlInitializationService.java

@@ -10,6 +10,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.action.admin.indices.alias.TransportIndicesAliasesAction;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesAction;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest;
@@ -173,7 +174,7 @@ public final class MlInitializationService implements ClusterStateListener {
         String[] mlHiddenIndexPatterns = MachineLearning.getMlHiddenIndexPatterns();
 
         // Step 5: Handle errors encountered on the way.
-        ActionListener<AcknowledgedResponse> finalListener = ActionListener.wrap(updateAliasesResponse -> {
+        ActionListener<IndicesAliasesResponse> finalListener = ActionListener.wrap(updateAliasesResponse -> {
             if (updateAliasesResponse.isAcknowledged() == false) {
                 logger.warn("One or more of the ML internal aliases could not be made hidden.");
                 return;
@@ -194,7 +195,7 @@ public final class MlInitializationService implements ClusterStateListener {
             }
             if (indicesAliasesRequest.getAliasActions().isEmpty()) {
                 logger.debug("There are no ML internal aliases that need to be made hidden, [{}]", getAliasesResponse.getAliases());
-                finalListener.onResponse(AcknowledgedResponse.TRUE);
+                finalListener.onResponse(IndicesAliasesResponse.ACKNOWLEDGED_NO_ERRORS);
                 return;
             }
             String indicesWithNonHiddenAliasesString = indicesAliasesRequest.getAliasActions()

+ 5 - 4
x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleter.java

@@ -10,6 +10,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesResponse;
 import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
@@ -287,7 +288,7 @@ public class JobDataDeleter {
 
         AtomicReference<String[]> indexNames = new AtomicReference<>();
 
-        final ActionListener<AcknowledgedResponse> completionHandler = ActionListener.wrap(
+        final ActionListener<IndicesAliasesResponse> completionHandler = ActionListener.wrap(
             response -> finishedHandler.accept(response.isAcknowledged()),
             failureHandler
         );
@@ -295,7 +296,7 @@ public class JobDataDeleter {
         // Step 9. If we did not drop the indices and after DBQ state done, we delete the aliases
         ActionListener<BulkByScrollResponse> dbqHandler = ActionListener.wrap(bulkByScrollResponse -> {
             if (bulkByScrollResponse == null) { // no action was taken by DBQ, assume indices were deleted
-                completionHandler.onResponse(AcknowledgedResponse.TRUE);
+                completionHandler.onResponse(IndicesAliasesResponse.ACKNOWLEDGED_NO_ERRORS);
             } else {
                 if (bulkByScrollResponse.isTimedOut()) {
                     logger.warn("[{}] DeleteByQuery for indices [{}] timed out.", jobId, String.join(", ", indexNames.get()));
@@ -469,7 +470,7 @@ public class JobDataDeleter {
         executeAsyncWithOrigin(client, ML_ORIGIN, RefreshAction.INSTANCE, refreshRequest, refreshListener);
     }
 
-    private void deleteAliases(@SuppressWarnings("HiddenField") String jobId, ActionListener<AcknowledgedResponse> finishedHandler) {
+    private void deleteAliases(@SuppressWarnings("HiddenField") String jobId, ActionListener<IndicesAliasesResponse> finishedHandler) {
         final String readAliasName = AnomalyDetectorsIndex.jobResultsAliasedName(jobId);
         final String writeAliasName = AnomalyDetectorsIndex.resultsWriteAlias(jobId);
 
@@ -486,7 +487,7 @@ public class JobDataDeleter {
                 if (removeRequest == null) {
                     // don't error if the job's aliases have already been deleted - carry on and delete the
                     // rest of the job's data
-                    finishedHandler.onResponse(AcknowledgedResponse.TRUE);
+                    finishedHandler.onResponse(IndicesAliasesResponse.ACKNOWLEDGED_NO_ERRORS);
                     return;
                 }
                 executeAsyncWithOrigin(

+ 2 - 1
x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java

@@ -16,6 +16,7 @@ import org.elasticsearch.ResourceNotFoundException;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.DelegatingActionListener;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
 import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
 import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest;
@@ -344,7 +345,7 @@ public class JobResultsProvider {
                 client.threadPool().getThreadContext(),
                 ML_ORIGIN,
                 request,
-                ActionListener.<AcknowledgedResponse>wrap(r -> finalListener.onResponse(true), finalListener::onFailure),
+                ActionListener.<IndicesAliasesResponse>wrap(r -> finalListener.onResponse(true), finalListener::onFailure),
                 client.admin().indices()::aliases
             );
         }, finalListener::onFailure);

+ 2 - 2
x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/TransformClusterStateListener.java

@@ -11,7 +11,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
-import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
 import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.cluster.ClusterChangedEvent;
 import org.elasticsearch.cluster.ClusterState;
@@ -97,7 +97,7 @@ class TransformClusterStateListener implements ClusterStateListener {
             client.threadPool().getThreadContext(),
             TRANSFORM_ORIGIN,
             request,
-            ActionListener.<AcknowledgedResponse>wrap(r -> finalListener.onResponse(r.isAcknowledged()), finalListener::onFailure),
+            ActionListener.<IndicesAliasesResponse>wrap(r -> finalListener.onResponse(r.isAcknowledged()), finalListener::onFailure),
             client.admin().indices()::aliases
         );
     }