Add delete_list endpoint (elastic/elasticsearch#409)

Adds a delete_list endpoint.  If a list is currently in use by a job, it is not allowed to be deleted

Original commit: elastic/x-pack-elasticsearch@7d9a984b3a
This commit is contained in:
Zachary Tong 2016-12-07 12:40:12 -05:00 committed by GitHub
parent af61a51e22
commit 3c711e6dff
5 changed files with 342 additions and 0 deletions

View File

@ -27,6 +27,7 @@ import org.elasticsearch.threadpool.FixedExecutorBuilder;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.prelert.action.DeleteJobAction;
import org.elasticsearch.xpack.prelert.action.DeleteListAction;
import org.elasticsearch.xpack.prelert.action.DeleteModelSnapshotAction;
import org.elasticsearch.xpack.prelert.action.GetBucketsAction;
import org.elasticsearch.xpack.prelert.action.GetCategoriesDefinitionAction;
@ -74,6 +75,7 @@ import org.elasticsearch.xpack.prelert.job.usage.UsageReporter;
import org.elasticsearch.xpack.prelert.rest.job.RestJobDataAction;
import org.elasticsearch.xpack.prelert.rest.job.RestCloseJobAction;
import org.elasticsearch.xpack.prelert.rest.job.RestFlushJobAction;
import org.elasticsearch.xpack.prelert.rest.list.RestDeleteListAction;
import org.elasticsearch.xpack.prelert.rest.results.RestGetInfluencersAction;
import org.elasticsearch.xpack.prelert.rest.job.RestDeleteJobAction;
import org.elasticsearch.xpack.prelert.rest.job.RestGetJobsAction;
@ -197,6 +199,7 @@ public class PrelertPlugin extends Plugin implements ActionPlugin {
RestOpenJobAction.class,
RestGetListAction.class,
RestPutListAction.class,
RestDeleteListAction.class,
RestGetInfluencersAction.class,
RestGetRecordsAction.class,
RestGetBucketsAction.class,
@ -227,6 +230,7 @@ public class PrelertPlugin extends Plugin implements ActionPlugin {
new ActionHandler<>(UpdateJobSchedulerStatusAction.INSTANCE, UpdateJobSchedulerStatusAction.TransportAction.class),
new ActionHandler<>(GetListAction.INSTANCE, GetListAction.TransportAction.class),
new ActionHandler<>(PutListAction.INSTANCE, PutListAction.TransportAction.class),
new ActionHandler<>(DeleteListAction.INSTANCE, DeleteListAction.TransportAction.class),
new ActionHandler<>(GetBucketsAction.INSTANCE, GetBucketsAction.TransportAction.class),
new ActionHandler<>(GetInfluencersAction.INSTANCE, GetInfluencersAction.TransportAction.class),
new ActionHandler<>(GetRecordsAction.INSTANCE, GetRecordsAction.TransportAction.class),

View File

@ -0,0 +1,219 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.prelert.action;
import org.elasticsearch.ResourceNotFoundException;
import org.elasticsearch.action.Action;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.delete.TransportDeleteAction;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.master.AcknowledgedRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.support.master.MasterNodeOperationRequestBuilder;
import org.elasticsearch.action.support.master.TransportMasterNodeAction;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.prelert.job.Detector;
import org.elasticsearch.xpack.prelert.job.Job;
import org.elasticsearch.xpack.prelert.job.messages.Messages;
import org.elasticsearch.xpack.prelert.job.metadata.PrelertMetadata;
import org.elasticsearch.xpack.prelert.lists.ListDocument;
import org.elasticsearch.xpack.prelert.utils.ExceptionsHelper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class DeleteListAction extends Action<DeleteListAction.Request, DeleteListAction.Response, DeleteListAction.RequestBuilder> {
public static final DeleteListAction INSTANCE = new DeleteListAction();
public static final String NAME = "cluster:admin/prelert/list/delete";
private DeleteListAction() {
super(NAME);
}
@Override
public RequestBuilder newRequestBuilder(ElasticsearchClient client) {
return new RequestBuilder(client, this);
}
@Override
public Response newResponse() {
return new Response();
}
public static class Request extends AcknowledgedRequest<Request> {
public static final ParseField LIST_ID = new ParseField("list_id");
private String listId;
Request() {
}
public Request(String listId) {
this.listId = ExceptionsHelper.requireNonNull(listId, LIST_ID.getPreferredName());
}
public String getListId() {
return listId;
}
@Override
public ActionRequestValidationException validate() {
return null;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
listId = in.readString();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(listId);
}
@Override
public int hashCode() {
return Objects.hash(listId);
}
@Override
public boolean equals(Object obj) {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Request other = (Request) obj;
return Objects.equals(listId, other.listId);
}
}
public static class RequestBuilder extends MasterNodeOperationRequestBuilder<Request, Response, RequestBuilder> {
public RequestBuilder(ElasticsearchClient client, DeleteListAction action) {
super(client, action, new Request());
}
}
public static class Response extends AcknowledgedResponse {
public Response(boolean acknowledged) {
super(acknowledged);
}
private Response() {}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
readAcknowledged(in);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
writeAcknowledged(out);
}
}
public static class TransportAction extends TransportMasterNodeAction<Request, Response> {
private final TransportDeleteAction transportAction;
// TODO these need to be moved to a settings object later. See #20
private static final String PRELERT_INFO_INDEX = "prelert-int";
@Inject
public TransportAction(Settings settings, TransportService transportService, ClusterService clusterService,
ThreadPool threadPool, ActionFilters actionFilters,
IndexNameExpressionResolver indexNameExpressionResolver,
TransportDeleteAction transportAction) {
super(settings, DeleteListAction.NAME, transportService, clusterService, threadPool, actionFilters,
indexNameExpressionResolver, Request::new);
this.transportAction = transportAction;
}
@Override
protected String executor() {
return ThreadPool.Names.SAME;
}
@Override
protected Response newResponse() {
return new Response();
}
@Override
protected void masterOperation(Request request, ClusterState state, ActionListener<Response> listener) throws Exception {
final String listId = request.getListId();
PrelertMetadata currentPrelertMetadata = state.metaData().custom(PrelertMetadata.TYPE);
Map<String, Job> jobs = currentPrelertMetadata.getJobs();
List<String> currentlyUsedBy = new ArrayList<>();
for (Job job : jobs.values()) {
List<Detector> detectors = job.getAnalysisConfig().getDetectors();
for (Detector detector : detectors) {
if (detector.extractReferencedLists().contains(listId)) {
currentlyUsedBy.add(job.getId());
break;
}
}
}
if (!currentlyUsedBy.isEmpty()) {
throw ExceptionsHelper.conflictStatusException("Cannot delete List, currently used by jobs: "
+ currentlyUsedBy);
}
DeleteRequest deleteRequest = new DeleteRequest(PRELERT_INFO_INDEX, ListDocument.TYPE.getPreferredName(), listId);
transportAction.execute(deleteRequest, new ActionListener<DeleteResponse>() {
@Override
public void onResponse(DeleteResponse deleteResponse) {
if (deleteResponse.status().equals(RestStatus.NOT_FOUND)) {
listener.onFailure(new ResourceNotFoundException("Could not delete list with ID [" + listId
+ "] because it does not exist"));
} else {
listener.onResponse(new Response(true));
}
}
@Override
public void onFailure(Exception e) {
logger.error("Could not delete list with ID [" + listId + "]", e);
listener.onFailure(new IllegalStateException("Could not delete list with ID [" + listId + "]", e));
}
});
}
@Override
protected ClusterBlockException checkBlock(Request request, ClusterState state) {
return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE);
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.prelert.rest.list;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.action.AcknowledgedRestListener;
import org.elasticsearch.xpack.prelert.PrelertPlugin;
import org.elasticsearch.xpack.prelert.action.DeleteListAction;
import org.elasticsearch.xpack.prelert.action.DeleteListAction.Request;
import org.elasticsearch.xpack.prelert.action.PutListAction;
import java.io.IOException;
public class RestDeleteListAction extends BaseRestHandler {
private final DeleteListAction.TransportAction transportAction;
@Inject
public RestDeleteListAction(Settings settings, RestController controller, DeleteListAction.TransportAction transportAction) {
super(settings);
this.transportAction = transportAction;
controller.registerHandler(RestRequest.Method.DELETE,
PrelertPlugin.BASE_PATH + "lists/{" + Request.LIST_ID.getPreferredName() + "}", this);
}
@Override
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
Request request = new Request(restRequest.param(Request.LIST_ID.getPreferredName()));
return channel -> transportAction.execute(request, new AcknowledgedRestListener<>(channel));
}
}

View File

@ -0,0 +1,17 @@
{
"xpack.prelert.delete_list": {
"methods": [ "DELETE" ],
"url": {
"path": "/_xpack/prelert/lists/{list_id}",
"paths": [ "/_xpack/prelert/lists/{list_id}" ],
"parts": {
"list_id": {
"type" : "string",
"required" : true,
"description" : "The ID of the list to delete"
}
}
},
"body": null
}
}

View File

@ -62,3 +62,65 @@ setup:
{
"items": ["abc", "xyz"]
}
---
"Test delete in-use list":
- do:
xpack.prelert.put_job:
body: >
{
"job_id":"farequote2",
"description":"Analysis of response time by airline",
"analysis_config" : {
"bucket_span":3600,
"detectors" :[{"function":"mean","field_name":"airline",
"detector_rules": [
{
"rule_conditions": [
{
"condition_type": "categorical",
"value_list": "foo"
}
]
}
]}]
},
"data_description" : {
"field_delimiter":",",
"time_field":"time",
"time_format":"yyyy-MM-dd HH:mm:ssX"
}
}
- do:
catch: conflict
xpack.prelert.delete_list:
list_id: "foo"
---
"Test non-existing list":
- do:
catch: missing
xpack.prelert.delete_list:
list_id: "does_not_exist"
---
"Test valid delete list":
- do:
xpack.prelert.get_lists:
list_id: "foo"
- match: { count: 1 }
- match:
lists.0:
id: "foo"
items: ["abc", "xyz"]
- do:
xpack.prelert.delete_list:
list_id: "foo"
- do:
catch: missing
xpack.prelert.get_lists:
list_id: "foo"