Skip to content

Commit c6cbc99

Browse files
[ML] Add ML filter update API (#31437)
This adds an api to allow updating a filter: POST _xpack/ml/filters/{filter_id}/_update The request body may have: - description: setting a new description - add_items: a list of the items to add - remove_items: a list of the items to remove This commit also changes the PUT filter api to error when the filter_id is already used. As now there is an api for updating filters, the put api should only be used to create new ones. Also, updating a filter results into a notification message auditing the change for every job that is using that filter.
1 parent f22f91c commit c6cbc99

37 files changed

+794
-72
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
import org.elasticsearch.xpack.core.ml.action.StopDatafeedAction;
8585
import org.elasticsearch.xpack.core.ml.action.UpdateCalendarJobAction;
8686
import org.elasticsearch.xpack.core.ml.action.UpdateDatafeedAction;
87+
import org.elasticsearch.xpack.core.ml.action.UpdateFilterAction;
8788
import org.elasticsearch.xpack.core.ml.action.UpdateJobAction;
8889
import org.elasticsearch.xpack.core.ml.action.UpdateModelSnapshotAction;
8990
import org.elasticsearch.xpack.core.ml.action.UpdateProcessAction;
@@ -220,6 +221,7 @@ public List<Action> getClientActions() {
220221
OpenJobAction.INSTANCE,
221222
GetFiltersAction.INSTANCE,
222223
PutFilterAction.INSTANCE,
224+
UpdateFilterAction.INSTANCE,
223225
DeleteFilterAction.INSTANCE,
224226
KillProcessAction.INSTANCE,
225227
GetBucketsAction.INSTANCE,
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.core.ml.action;
7+
8+
import org.elasticsearch.action.Action;
9+
import org.elasticsearch.action.ActionRequest;
10+
import org.elasticsearch.action.ActionRequestBuilder;
11+
import org.elasticsearch.action.ActionRequestValidationException;
12+
import org.elasticsearch.client.ElasticsearchClient;
13+
import org.elasticsearch.common.Nullable;
14+
import org.elasticsearch.common.ParseField;
15+
import org.elasticsearch.common.Strings;
16+
import org.elasticsearch.common.io.stream.StreamInput;
17+
import org.elasticsearch.common.io.stream.StreamOutput;
18+
import org.elasticsearch.common.xcontent.ObjectParser;
19+
import org.elasticsearch.common.xcontent.ToXContentObject;
20+
import org.elasticsearch.common.xcontent.XContentBuilder;
21+
import org.elasticsearch.common.xcontent.XContentParser;
22+
import org.elasticsearch.xpack.core.ml.job.config.MlFilter;
23+
import org.elasticsearch.xpack.core.ml.job.messages.Messages;
24+
import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper;
25+
26+
import java.io.IOException;
27+
import java.util.Arrays;
28+
import java.util.Collection;
29+
import java.util.Collections;
30+
import java.util.Objects;
31+
import java.util.SortedSet;
32+
import java.util.TreeSet;
33+
34+
35+
public class UpdateFilterAction extends Action<PutFilterAction.Response> {
36+
37+
public static final UpdateFilterAction INSTANCE = new UpdateFilterAction();
38+
public static final String NAME = "cluster:admin/xpack/ml/filters/update";
39+
40+
private UpdateFilterAction() {
41+
super(NAME);
42+
}
43+
44+
@Override
45+
public PutFilterAction.Response newResponse() {
46+
return new PutFilterAction.Response();
47+
}
48+
49+
public static class Request extends ActionRequest implements ToXContentObject {
50+
51+
public static final ParseField ADD_ITEMS = new ParseField("add_items");
52+
public static final ParseField REMOVE_ITEMS = new ParseField("remove_items");
53+
54+
private static final ObjectParser<Request, Void> PARSER = new ObjectParser<>(NAME, Request::new);
55+
56+
static {
57+
PARSER.declareString((request, filterId) -> request.filterId = filterId, MlFilter.ID);
58+
PARSER.declareStringOrNull(Request::setDescription, MlFilter.DESCRIPTION);
59+
PARSER.declareStringArray(Request::setAddItems, ADD_ITEMS);
60+
PARSER.declareStringArray(Request::setRemoveItems, REMOVE_ITEMS);
61+
}
62+
63+
public static Request parseRequest(String filterId, XContentParser parser) {
64+
Request request = PARSER.apply(parser, null);
65+
if (request.filterId == null) {
66+
request.filterId = filterId;
67+
} else if (!Strings.isNullOrEmpty(filterId) && !filterId.equals(request.filterId)) {
68+
// If we have both URI and body filter ID, they must be identical
69+
throw new IllegalArgumentException(Messages.getMessage(Messages.INCONSISTENT_ID, MlFilter.ID.getPreferredName(),
70+
request.filterId, filterId));
71+
}
72+
return request;
73+
}
74+
75+
private String filterId;
76+
@Nullable
77+
private String description;
78+
private SortedSet<String> addItems = Collections.emptySortedSet();
79+
private SortedSet<String> removeItems = Collections.emptySortedSet();
80+
81+
public Request() {
82+
}
83+
84+
public Request(String filterId) {
85+
this.filterId = ExceptionsHelper.requireNonNull(filterId, MlFilter.ID.getPreferredName());
86+
}
87+
88+
public String getFilterId() {
89+
return filterId;
90+
}
91+
92+
public String getDescription() {
93+
return description;
94+
}
95+
96+
public void setDescription(String description) {
97+
this.description = description;
98+
}
99+
100+
public SortedSet<String> getAddItems() {
101+
return addItems;
102+
}
103+
104+
public void setAddItems(Collection<String> addItems) {
105+
this.addItems = new TreeSet<>(ExceptionsHelper.requireNonNull(addItems, ADD_ITEMS.getPreferredName()));
106+
}
107+
108+
public SortedSet<String> getRemoveItems() {
109+
return removeItems;
110+
}
111+
112+
public void setRemoveItems(Collection<String> removeItems) {
113+
this.removeItems = new TreeSet<>(ExceptionsHelper.requireNonNull(removeItems, REMOVE_ITEMS.getPreferredName()));
114+
}
115+
116+
public boolean isNoop() {
117+
return description == null && addItems.isEmpty() && removeItems.isEmpty();
118+
}
119+
120+
@Override
121+
public ActionRequestValidationException validate() {
122+
return null;
123+
}
124+
125+
@Override
126+
public void readFrom(StreamInput in) throws IOException {
127+
super.readFrom(in);
128+
filterId = in.readString();
129+
description = in.readOptionalString();
130+
addItems = new TreeSet<>(Arrays.asList(in.readStringArray()));
131+
removeItems = new TreeSet<>(Arrays.asList(in.readStringArray()));
132+
}
133+
134+
@Override
135+
public void writeTo(StreamOutput out) throws IOException {
136+
super.writeTo(out);
137+
out.writeString(filterId);
138+
out.writeOptionalString(description);
139+
out.writeStringArray(addItems.toArray(new String[addItems.size()]));
140+
out.writeStringArray(removeItems.toArray(new String[removeItems.size()]));
141+
}
142+
143+
@Override
144+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
145+
builder.startObject();
146+
builder.field(MlFilter.ID.getPreferredName(), filterId);
147+
if (description != null) {
148+
builder.field(MlFilter.DESCRIPTION.getPreferredName(), description);
149+
}
150+
if (addItems.isEmpty() == false) {
151+
builder.field(ADD_ITEMS.getPreferredName(), addItems);
152+
}
153+
if (removeItems.isEmpty() == false) {
154+
builder.field(REMOVE_ITEMS.getPreferredName(), removeItems);
155+
}
156+
builder.endObject();
157+
return builder;
158+
}
159+
160+
@Override
161+
public int hashCode() {
162+
return Objects.hash(filterId, description, addItems, removeItems);
163+
}
164+
165+
@Override
166+
public boolean equals(Object obj) {
167+
if (obj == null) {
168+
return false;
169+
}
170+
if (getClass() != obj.getClass()) {
171+
return false;
172+
}
173+
Request other = (Request) obj;
174+
return Objects.equals(filterId, other.filterId)
175+
&& Objects.equals(description, other.description)
176+
&& Objects.equals(addItems, other.addItems)
177+
&& Objects.equals(removeItems, other.removeItems);
178+
}
179+
}
180+
181+
public static class RequestBuilder extends ActionRequestBuilder<Request, PutFilterAction.Response> {
182+
183+
public RequestBuilder(ElasticsearchClient client) {
184+
super(client, INSTANCE, new Request());
185+
}
186+
}
187+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/MlFilter.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private static ObjectParser<Builder, Void> createParser(boolean ignoreUnknownFie
5656
private final String description;
5757
private final SortedSet<String> items;
5858

59-
public MlFilter(String id, String description, SortedSet<String> items) {
59+
private MlFilter(String id, String description, SortedSet<String> items) {
6060
this.id = Objects.requireNonNull(id, ID.getPreferredName() + " must not be null");
6161
this.description = description;
6262
this.items = Objects.requireNonNull(items, ITEMS.getPreferredName() + " must not be null");
@@ -69,8 +69,7 @@ public MlFilter(StreamInput in) throws IOException {
6969
} else {
7070
description = null;
7171
}
72-
items = new TreeSet<>();
73-
items.addAll(Arrays.asList(in.readStringArray()));
72+
items = new TreeSet<>(Arrays.asList(in.readStringArray()));
7473
}
7574

7675
@Override
@@ -163,9 +162,13 @@ public Builder setDescription(String description) {
163162
return this;
164163
}
165164

165+
public Builder setItems(SortedSet<String> items) {
166+
this.items = items;
167+
return this;
168+
}
169+
166170
public Builder setItems(List<String> items) {
167-
this.items = new TreeSet<>();
168-
this.items.addAll(items);
171+
this.items = new TreeSet<>(items);
169172
return this;
170173
}
171174

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public final class Messages {
4242
public static final String DATAFEED_FREQUENCY_MUST_BE_MULTIPLE_OF_AGGREGATIONS_INTERVAL =
4343
"Datafeed frequency [{0}] must be a multiple of the aggregation interval [{1}]";
4444

45+
public static final String FILTER_NOT_FOUND = "No filter with id [{0}] exists";
46+
4547
public static final String INCONSISTENT_ID =
4648
"Inconsistent {0}; ''{1}'' specified in the body differs from ''{2}'' specified as a URL argument";
4749
public static final String INVALID_ID = "Invalid {0}; ''{1}'' can contain lowercase alphanumeric (a-z and 0-9), hyphens or " +

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/state/ModelSnapshot.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
import org.elasticsearch.common.xcontent.ToXContentObject;
2020
import org.elasticsearch.common.xcontent.XContentBuilder;
2121
import org.elasticsearch.common.xcontent.XContentFactory;
22-
import org.elasticsearch.common.xcontent.XContentHelper;
2322
import org.elasticsearch.common.xcontent.XContentParser;
2423
import org.elasticsearch.common.xcontent.XContentParser.Token;
24+
import org.elasticsearch.common.xcontent.XContentType;
2525
import org.elasticsearch.xpack.core.ml.job.config.Job;
2626
import org.elasticsearch.xpack.core.ml.utils.time.TimeUtils;
2727

@@ -345,7 +345,7 @@ public static String v54DocumentId(String jobId, String snapshotId) {
345345

346346
public static ModelSnapshot fromJson(BytesReference bytesReference) {
347347
try (InputStream stream = bytesReference.streamInput();
348-
XContentParser parser = XContentFactory.xContent(XContentHelper.xContentType(bytesReference))
348+
XContentParser parser = XContentFactory.xContent(XContentType.JSON)
349349
.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) {
350350
return LENIENT_PARSER.apply(parser, null).build();
351351
} catch (IOException e) {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/ExceptionsHelper.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ public static ElasticsearchException serverError(String msg, Throwable cause) {
3838
return new ElasticsearchException(msg, cause);
3939
}
4040

41+
public static ElasticsearchStatusException conflictStatusException(String msg, Throwable cause, Object... args) {
42+
return new ElasticsearchStatusException(msg, RestStatus.CONFLICT, cause, args);
43+
}
44+
4145
public static ElasticsearchStatusException conflictStatusException(String msg, Object... args) {
4246
return new ElasticsearchStatusException(msg, RestStatus.CONFLICT, args);
4347
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.core.ml.action;
7+
8+
import org.elasticsearch.common.xcontent.XContentParser;
9+
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
10+
import org.elasticsearch.xpack.core.ml.action.UpdateFilterAction.Request;
11+
12+
import java.util.ArrayList;
13+
import java.util.Collection;
14+
import java.util.List;
15+
16+
public class UpdateFilterActionRequestTests extends AbstractStreamableXContentTestCase<Request> {
17+
18+
private String filterId = randomAlphaOfLength(20);
19+
20+
@Override
21+
protected Request createTestInstance() {
22+
UpdateFilterAction.Request request = new UpdateFilterAction.Request(filterId);
23+
if (randomBoolean()) {
24+
request.setDescription(randomAlphaOfLength(20));
25+
}
26+
if (randomBoolean()) {
27+
request.setAddItems(generateRandomStrings());
28+
}
29+
if (randomBoolean()) {
30+
request.setRemoveItems(generateRandomStrings());
31+
}
32+
return request;
33+
}
34+
35+
private static Collection<String> generateRandomStrings() {
36+
int size = randomIntBetween(0, 10);
37+
List<String> strings = new ArrayList<>(size);
38+
for (int i = 0; i < size; ++i) {
39+
strings.add(randomAlphaOfLength(20));
40+
}
41+
return strings;
42+
}
43+
44+
@Override
45+
protected boolean supportsUnknownFields() {
46+
return false;
47+
}
48+
49+
@Override
50+
protected Request createBlankInstance() {
51+
return new Request();
52+
}
53+
54+
@Override
55+
protected Request doParseInstance(XContentParser parser) {
56+
return Request.parseRequest(filterId, parser);
57+
}
58+
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/config/MlFilterTests.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.test.AbstractSerializingTestCase;
1212

1313
import java.io.IOException;
14+
import java.util.SortedSet;
1415
import java.util.TreeSet;
1516

1617
import static org.hamcrest.Matchers.contains;
@@ -43,7 +44,7 @@ public static MlFilter createRandom(String filterId) {
4344
for (int i = 0; i < size; i++) {
4445
items.add(randomAlphaOfLengthBetween(1, 20));
4546
}
46-
return new MlFilter(filterId, description, items);
47+
return MlFilter.builder(filterId).setDescription(description).setItems(items).build();
4748
}
4849

4950
@Override
@@ -57,13 +58,13 @@ protected MlFilter doParseInstance(XContentParser parser) {
5758
}
5859

5960
public void testNullId() {
60-
NullPointerException ex = expectThrows(NullPointerException.class, () -> new MlFilter(null, "", new TreeSet<>()));
61+
NullPointerException ex = expectThrows(NullPointerException.class, () -> MlFilter.builder(null).build());
6162
assertEquals(MlFilter.ID.getPreferredName() + " must not be null", ex.getMessage());
6263
}
6364

6465
public void testNullItems() {
65-
NullPointerException ex =
66-
expectThrows(NullPointerException.class, () -> new MlFilter(randomAlphaOfLengthBetween(1, 20), "", null));
66+
NullPointerException ex = expectThrows(NullPointerException.class,
67+
() -> MlFilter.builder(randomAlphaOfLength(20)).setItems((SortedSet<String>) null).build());
6768
assertEquals(MlFilter.ITEMS.getPreferredName() + " must not be null", ex.getMessage());
6869
}
6970

0 commit comments

Comments
 (0)