diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java index b90fe870bb3..8d3e5e8c135 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java @@ -52,6 +52,7 @@ import org.elasticsearch.xpack.XPackFeatureSet; import org.elasticsearch.xpack.XPackPlugin; import org.elasticsearch.xpack.XPackSettings; import org.elasticsearch.xpack.ml.action.CloseJobAction; +import org.elasticsearch.xpack.ml.action.DeleteCalendarAction; import org.elasticsearch.xpack.ml.action.DeleteDatafeedAction; import org.elasticsearch.xpack.ml.action.DeleteExpiredDataAction; import org.elasticsearch.xpack.ml.action.DeleteFilterAction; @@ -61,6 +62,7 @@ import org.elasticsearch.xpack.ml.action.FinalizeJobExecutionAction; import org.elasticsearch.xpack.ml.action.FlushJobAction; import org.elasticsearch.xpack.ml.action.ForecastJobAction; import org.elasticsearch.xpack.ml.action.GetBucketsAction; +import org.elasticsearch.xpack.ml.action.GetCalendarsAction; import org.elasticsearch.xpack.ml.action.GetCategoriesAction; import org.elasticsearch.xpack.ml.action.GetDatafeedsAction; import org.elasticsearch.xpack.ml.action.GetDatafeedsStatsAction; @@ -76,6 +78,7 @@ import org.elasticsearch.xpack.ml.action.KillProcessAction; import org.elasticsearch.xpack.ml.action.OpenJobAction; import org.elasticsearch.xpack.ml.action.PostDataAction; import org.elasticsearch.xpack.ml.action.PreviewDatafeedAction; +import org.elasticsearch.xpack.ml.action.PutCalendarAction; import org.elasticsearch.xpack.ml.action.PutDatafeedAction; import org.elasticsearch.xpack.ml.action.PutFilterAction; import org.elasticsearch.xpack.ml.action.PutJobAction; @@ -114,6 +117,9 @@ import org.elasticsearch.xpack.ml.job.process.normalizer.NormalizerProcessFactor import org.elasticsearch.xpack.ml.notifications.AuditMessage; import org.elasticsearch.xpack.ml.notifications.Auditor; import org.elasticsearch.xpack.ml.rest.RestDeleteExpiredDataAction; +import org.elasticsearch.xpack.ml.rest.calendar.RestDeleteCalendarAction; +import org.elasticsearch.xpack.ml.rest.calendar.RestGetCalendarsAction; +import org.elasticsearch.xpack.ml.rest.calendar.RestPutCalendarAction; import org.elasticsearch.xpack.ml.rest.datafeeds.RestDeleteDatafeedAction; import org.elasticsearch.xpack.ml.rest.datafeeds.RestGetDatafeedStatsAction; import org.elasticsearch.xpack.ml.rest.datafeeds.RestGetDatafeedsAction; @@ -458,7 +464,10 @@ public class MachineLearning implements ActionPlugin { new RestStopDatafeedAction(settings, restController), new RestDeleteModelSnapshotAction(settings, restController), new RestDeleteExpiredDataAction(settings, restController), - new RestForecastJobAction(settings, restController) + new RestForecastJobAction(settings, restController), + new RestGetCalendarsAction(settings, restController), + new RestPutCalendarAction(settings, restController), + new RestDeleteCalendarAction(settings, restController) ); } @@ -504,7 +513,10 @@ public class MachineLearning implements ActionPlugin { new ActionHandler<>(DeleteModelSnapshotAction.INSTANCE, DeleteModelSnapshotAction.TransportAction.class), new ActionHandler<>(UpdateProcessAction.INSTANCE, UpdateProcessAction.TransportAction.class), new ActionHandler<>(DeleteExpiredDataAction.INSTANCE, DeleteExpiredDataAction.TransportAction.class), - new ActionHandler<>(ForecastJobAction.INSTANCE, ForecastJobAction.TransportAction.class) + new ActionHandler<>(ForecastJobAction.INSTANCE, ForecastJobAction.TransportAction.class), + new ActionHandler<>(GetCalendarsAction.INSTANCE, GetCalendarsAction.TransportAction.class), + new ActionHandler<>(PutCalendarAction.INSTANCE, PutCalendarAction.TransportAction.class), + new ActionHandler<>(DeleteCalendarAction.INSTANCE, DeleteCalendarAction.TransportAction.class) ); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/MlMetaIndex.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/MlMetaIndex.java index 4139f51ab37..95e79cea92e 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/ml/MlMetaIndex.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/MlMetaIndex.java @@ -20,6 +20,8 @@ public final class MlMetaIndex { */ public static final String INDEX_NAME = ".ml-meta"; + public static final String INCLUDE_TYPE_KEY = "include_type"; + public static final String TYPE = "doc"; private MlMetaIndex() {} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/action/DeleteCalendarAction.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/action/DeleteCalendarAction.java new file mode 100644 index 00000000000..1fd089d9685 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/action/DeleteCalendarAction.java @@ -0,0 +1,184 @@ +/* + * 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.ml.action; + +import org.elasticsearch.ResourceNotFoundException; +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.bulk.BulkAction; +import org.elasticsearch.action.bulk.BulkRequestBuilder; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.delete.DeleteRequest; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.action.support.master.AcknowledgedRequest; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +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.ml.MlMetaIndex; +import org.elasticsearch.xpack.ml.calendars.Calendar; +import org.elasticsearch.xpack.ml.utils.ExceptionsHelper; + +import java.io.IOException; +import java.util.Objects; + +import static org.elasticsearch.xpack.ClientHelper.ML_ORIGIN; +import static org.elasticsearch.xpack.ClientHelper.executeAsyncWithOrigin; + +public class DeleteCalendarAction extends Action { + + public static final DeleteCalendarAction INSTANCE = new DeleteCalendarAction(); + public static final String NAME = "cluster:admin/xpack/ml/calendars/delete"; + + private DeleteCalendarAction() { + 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 { + + + private String calendarId; + + Request() { + + } + + public Request(String calendarId) { + this.calendarId = ExceptionsHelper.requireNonNull(calendarId, Calendar.ID.getPreferredName()); + } + + public String getCalendarId() { + return calendarId; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + calendarId = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(calendarId); + } + + @Override + public int hashCode() { + return Objects.hash(calendarId); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + Request other = (Request) obj; + return Objects.equals(calendarId, other.calendarId); + } + } + + public static class RequestBuilder extends ActionRequestBuilder { + + public RequestBuilder(ElasticsearchClient client, DeleteCalendarAction 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 HandledTransportAction { + + private final Client client; + + @Inject + public TransportAction(Settings settings, ThreadPool threadPool, + TransportService transportService, ActionFilters actionFilters, + IndexNameExpressionResolver indexNameExpressionResolver, + Client client) { + super(settings, NAME, threadPool, transportService, actionFilters, + indexNameExpressionResolver, Request::new); + this.client = client; + } + + @Override + protected void doExecute(DeleteCalendarAction.Request request, ActionListener listener) { + + final String calendarId = request.getCalendarId(); + + DeleteRequest deleteRequest = new DeleteRequest(MlMetaIndex.INDEX_NAME, MlMetaIndex.TYPE, Calendar.documentId(calendarId)); + + BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + bulkRequestBuilder.add(deleteRequest); + bulkRequestBuilder.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + executeAsyncWithOrigin(client, ML_ORIGIN, BulkAction.INSTANCE, bulkRequestBuilder.request(), + new ActionListener() { + @Override + public void onResponse(BulkResponse bulkResponse) { + if (bulkResponse.getItems()[0].status() == RestStatus.NOT_FOUND) { + listener.onFailure(new ResourceNotFoundException("Could not delete calendar with ID [" + calendarId + + "] because it does not exist")); + } else { + listener.onResponse(new Response(true)); + } + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(ExceptionsHelper.serverError("Could not delete calendar with ID [" + calendarId + "]", e)); + } + }); + } + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/action/GetCalendarsAction.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/action/GetCalendarsAction.java new file mode 100644 index 00000000000..aafb33d2aa1 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/action/GetCalendarsAction.java @@ -0,0 +1,314 @@ +/* + * 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.ml.action; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.get.GetAction; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +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.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.StatusToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.ml.MlMetaIndex; +import org.elasticsearch.xpack.ml.action.util.PageParams; +import org.elasticsearch.xpack.ml.action.util.QueryPage; +import org.elasticsearch.xpack.ml.calendars.Calendar; +import org.elasticsearch.xpack.ml.job.persistence.JobProvider; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.xpack.ClientHelper.ML_ORIGIN; +import static org.elasticsearch.xpack.ClientHelper.executeAsyncWithOrigin; + +public class GetCalendarsAction extends Action { + + public static final GetCalendarsAction INSTANCE = new GetCalendarsAction(); + public static final String NAME = "cluster:monitor/xpack/ml/calendars/get"; + + private GetCalendarsAction() { + super(NAME); + } + + @Override + public RequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new RequestBuilder(client); + } + + @Override + public Response newResponse() { + return new Response(); + } + + public static class Request extends ActionRequest { + + private String calendarId; + private PageParams pageParams; + + public Request() { + } + + public void setCalendarId(String calendarId) { + this.calendarId = calendarId; + } + + public String getCalendarId() { + return calendarId; + } + + public PageParams getPageParams() { + return pageParams; + } + + public void setPageParams(PageParams pageParams) { + this.pageParams = pageParams; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + + if (calendarId != null && pageParams != null) { + validationException = addValidationError("Params [" + PageParams.FROM.getPreferredName() + + ", " + PageParams.SIZE.getPreferredName() + "] are incompatible with [" + + Calendar.ID.getPreferredName() + "].", + validationException); + } + return validationException; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + calendarId = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(calendarId); + } + + @Override + public int hashCode() { + return Objects.hash(calendarId); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Request other = (Request) obj; + return Objects.equals(calendarId, other.calendarId); + } + } + + public static class RequestBuilder extends ActionRequestBuilder { + + public RequestBuilder(ElasticsearchClient client) { + super(client, INSTANCE, new Request()); + } + } + + public static class Response extends ActionResponse implements StatusToXContentObject { + + private QueryPage calendars; + + public Response(QueryPage calendars) { + this.calendars = calendars; + } + + Response() { + } + + public QueryPage getCalendars() { + return calendars; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + calendars = new QueryPage<>(in, Calendar::new); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + calendars.writeTo(out); + } + + @Override + public RestStatus status() { + return RestStatus.OK; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + calendars.doXContentBody(builder, params); + builder.endObject(); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(calendars); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Response other = (Response) obj; + return Objects.equals(calendars, other.calendars); + } + + @Override + public final String toString() { + return Strings.toString(this); + } + } + + public static class TransportAction extends HandledTransportAction { + + private final Client client; + + @Inject + public TransportAction(Settings settings, ThreadPool threadPool, + TransportService transportService, ActionFilters actionFilters, + IndexNameExpressionResolver indexNameExpressionResolver, + Client client) { + super(settings, NAME, threadPool, transportService, actionFilters, + indexNameExpressionResolver, Request::new); + this.client = client; + } + + @Override + protected void doExecute(Request request, ActionListener listener) { + final String calendarId = request.getCalendarId(); + if (request.getCalendarId() != null) { + getCalendar(calendarId, listener); + } else { + PageParams pageParams = request.getPageParams(); + if (pageParams == null) { + pageParams = PageParams.defaultParams(); + } + getCalendars(pageParams, listener); + } + } + + private void getCalendar(String calendarId, ActionListener listener) { + GetRequest getRequest = new GetRequest(MlMetaIndex.INDEX_NAME, MlMetaIndex.TYPE, Calendar.documentId(calendarId)); + executeAsyncWithOrigin(client, ML_ORIGIN, GetAction.INSTANCE, getRequest, new ActionListener() { + @Override + public void onResponse(GetResponse getDocResponse) { + + try { + QueryPage calendars; + if (getDocResponse.isExists()) { + BytesReference docSource = getDocResponse.getSourceAsBytesRef(); + + try (XContentParser parser = + XContentFactory.xContent(docSource).createParser(NamedXContentRegistry.EMPTY, docSource)) { + Calendar calendar = Calendar.PARSER.apply(parser, null).build(); + calendars = new QueryPage<>(Collections.singletonList(calendar), 1, Calendar.RESULTS_FIELD); + + Response response = new Response(calendars); + listener.onResponse(response); + } + } else { + this.onFailure(QueryPage.emptyQueryPage(Calendar.RESULTS_FIELD)); + } + + } catch (Exception e) { + this.onFailure(e); + } + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); + } + + private void getCalendars(PageParams pageParams, ActionListener listener) { + SearchSourceBuilder sourceBuilder = new SearchSourceBuilder() + .from(pageParams.getFrom()) + .size(pageParams.getSize()) + .sort(Calendar.ID.getPreferredName()) + .query(QueryBuilders.termQuery(Calendar.TYPE.getPreferredName(), Calendar.CALENDAR_TYPE)); + + SearchRequest searchRequest = new SearchRequest(MlMetaIndex.INDEX_NAME) + .indicesOptions(JobProvider.addIgnoreUnavailable(SearchRequest.DEFAULT_INDICES_OPTIONS)) + .source(sourceBuilder); + + executeAsyncWithOrigin(client.threadPool().getThreadContext(), ML_ORIGIN, searchRequest, new ActionListener() { + @Override + public void onResponse(SearchResponse response) { + List docs = new ArrayList<>(); + for (SearchHit hit : response.getHits().getHits()) { + BytesReference docSource = hit.getSourceRef(); + try (XContentParser parser = XContentFactory.xContent(docSource).createParser( + NamedXContentRegistry.EMPTY, docSource)) { + docs.add(Calendar.PARSER.apply(parser, null).build()); + } catch (IOException e) { + this.onFailure(e); + } + } + + Response getResponse = new Response( + new QueryPage<>(docs, docs.size(), Calendar.RESULTS_FIELD)); + listener.onResponse(getResponse); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }, + client::search); + } + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/action/PutCalendarAction.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/action/PutCalendarAction.java new file mode 100644 index 00000000000..22a1e6a06f3 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/action/PutCalendarAction.java @@ -0,0 +1,221 @@ +/* + * 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.ml.action; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.Strings; +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.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.ml.MlMetaIndex; +import org.elasticsearch.xpack.ml.calendars.Calendar; +import org.elasticsearch.xpack.ml.job.messages.Messages; +import org.elasticsearch.xpack.ml.utils.ExceptionsHelper; + +import java.io.IOException; +import java.util.Collections; +import java.util.Objects; + +import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.xpack.ClientHelper.ML_ORIGIN; +import static org.elasticsearch.xpack.ClientHelper.executeAsyncWithOrigin; + +public class PutCalendarAction extends Action { + public static final PutCalendarAction INSTANCE = new PutCalendarAction(); + public static final String NAME = "cluster:admin/xpack/ml/calendars/put"; + + private PutCalendarAction() { + super(NAME); + } + + @Override + public RequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new RequestBuilder(client); + } + + @Override + public Response newResponse() { + return new Response(); + } + + public static class Request extends ActionRequest implements ToXContentObject { + + public static Request parseRequest(String calendarId, XContentParser parser) { + Calendar.Builder builder = Calendar.PARSER.apply(parser, null); + if (builder.getId() == null) { + builder.setId(calendarId); + } else if (!Strings.isNullOrEmpty(calendarId) && !calendarId.equals(builder.getId())) { + // If we have both URI and body filter ID, they must be identical + throw new IllegalArgumentException(Messages.getMessage(Messages.INCONSISTENT_ID, Calendar.ID.getPreferredName(), + builder.getId(), calendarId)); + } + return new Request(builder.build()); + } + + private Calendar calendar; + + Request() { + + } + + public Request(Calendar calendar) { + this.calendar = ExceptionsHelper.requireNonNull(calendar, "calendar"); + } + + public Calendar getCalendar() { + return calendar; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if ("_all".equals(calendar.getId())) { + validationException = + addValidationError("Cannot create a Calendar with the reserved name [_all]", + validationException); + } + return validationException; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + calendar = new Calendar(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + calendar.writeTo(out); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + calendar.toXContent(builder, params); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(calendar); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Request other = (Request) obj; + return Objects.equals(calendar, other.calendar); + } + } + + public static class RequestBuilder extends ActionRequestBuilder { + + public RequestBuilder(ElasticsearchClient client) { + super(client, INSTANCE, new Request()); + } + } + + public static class Response extends AcknowledgedResponse implements ToXContentObject { + + private Calendar calendar; + + Response() { + } + + public Response(Calendar calendar) { + super(true); + this.calendar = calendar; + } + + @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); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return calendar.toXContent(builder, params); + } + } + + public static class TransportAction extends HandledTransportAction { + + private final Client client; + + @Inject + public TransportAction(Settings settings, ThreadPool threadPool, + TransportService transportService, ActionFilters actionFilters, + IndexNameExpressionResolver indexNameExpressionResolver, Client client) { + super(settings, NAME, threadPool, transportService, actionFilters, + indexNameExpressionResolver, Request::new); + this.client = client; + } + + @Override + protected void doExecute(Request request, ActionListener listener) { + final Calendar calendar = request.getCalendar(); + IndexRequest indexRequest = new IndexRequest(MlMetaIndex.INDEX_NAME, MlMetaIndex.TYPE, calendar.documentId()); + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + indexRequest.source(calendar.toXContent(builder, + new ToXContent.MapParams(Collections.singletonMap(MlMetaIndex.INCLUDE_TYPE_KEY, "true")))); + } catch (IOException e) { + throw new IllegalStateException("Failed to serialise calendar with id [" + calendar.getId() + "]", e); + } + + // Make it an error to overwrite an existing calendar + indexRequest.opType(DocWriteRequest.OpType.CREATE); + indexRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + + executeAsyncWithOrigin(client, ML_ORIGIN, IndexAction.INSTANCE, indexRequest, + new ActionListener() { + @Override + public void onResponse(IndexResponse indexResponse) { + listener.onResponse(new Response(calendar)); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); + } + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/action/PutFilterAction.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/action/PutFilterAction.java index 86e070dcb23..d3df1004dd3 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/ml/action/PutFilterAction.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/action/PutFilterAction.java @@ -181,7 +181,7 @@ public class PutFilterAction extends Action PARSER = new ConstructingObjectParser<>(PAGE.getPreferredName(), a -> new PageParams(a[0] == null ? DEFAULT_FROM : (int) a[0], a[1] == null ? DEFAULT_SIZE : (int) a[1])); @@ -39,6 +38,10 @@ public class PageParams implements ToXContentObject, Writeable { private final int from; private final int size; + public static PageParams defaultParams() { + return new PageParams(DEFAULT_FROM, DEFAULT_SIZE); + } + public PageParams(StreamInput in) throws IOException { this(in.readVInt(), in.readVInt()); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/calendars/Calendar.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/calendars/Calendar.java new file mode 100644 index 00000000000..ed1d2c5f093 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/calendars/Calendar.java @@ -0,0 +1,139 @@ +/* + * 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.ml.calendars; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.xpack.ml.MlMetaIndex; +import org.elasticsearch.xpack.ml.job.config.MlFilter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class Calendar implements ToXContentObject, Writeable { + + public static final String CALENDAR_TYPE = "calendar"; + + public static final ParseField TYPE = new ParseField("type"); + public static final ParseField ID = new ParseField("calendar_id"); + public static final ParseField JOB_IDS = new ParseField("job_ids"); + + private static final String DOCUMENT_ID_PREFIX = "calendar_"; + + // For QueryPage + public static final ParseField RESULTS_FIELD = new ParseField("calendars"); + + public static final ObjectParser PARSER = + new ObjectParser<>(ID.getPreferredName(), Calendar.Builder::new); + + static { + PARSER.declareString(Calendar.Builder::setId, ID); + PARSER.declareStringArray(Calendar.Builder::setJobIds, JOB_IDS); + PARSER.declareString((builder, s) -> {}, TYPE); + } + + public static String documentId(String calendarId) { + return DOCUMENT_ID_PREFIX + calendarId; + } + + private final String id; + private final List jobIds; + + public Calendar(String id, List jobIds) { + this.id = Objects.requireNonNull(id, ID.getPreferredName() + " must not be null"); + this.jobIds = Objects.requireNonNull(jobIds, JOB_IDS.getPreferredName() + " must not be null"); + } + + public Calendar(StreamInput in) throws IOException { + id = in.readString(); + jobIds = Arrays.asList(in.readStringArray()); + } + + public String getId() { + return id; + } + + public String documentId() { + return documentId(id); + } + + public List getJobIds() { + return new ArrayList<>(jobIds); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(id); + out.writeStringArray(jobIds.toArray(new String[jobIds.size()])); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(ID.getPreferredName(), id); + builder.field(JOB_IDS.getPreferredName(), jobIds); + if (params.paramAsBoolean(MlMetaIndex.INCLUDE_TYPE_KEY, false)) { + builder.field(TYPE.getPreferredName(), CALENDAR_TYPE); + } + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof Calendar)) { + return false; + } + + Calendar other = (Calendar) obj; + return id.equals(other.id) && jobIds.equals(other.jobIds); + } + + @Override + public int hashCode() { + return Objects.hash(id, jobIds); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String calendarId; + private List jobIds = Collections.emptyList(); + + public String getId() { + return this.calendarId; + } + + public void setId(String calendarId) { + this.calendarId = calendarId; + } + + public Builder setJobIds(List jobIds) { + this.jobIds = jobIds; + return this; + } + + public Calendar build() { + return new Calendar(calendarId, jobIds); + } + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/calendars/SpecialEvent.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/calendars/SpecialEvent.java index 7c57ea31233..339ca80351a 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/ml/calendars/SpecialEvent.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/calendars/SpecialEvent.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.xpack.ml.MlMetaIndex; import org.elasticsearch.xpack.ml.job.config.Connective; import org.elasticsearch.xpack.ml.job.config.DetectionRule; import org.elasticsearch.xpack.ml.job.config.Operator; @@ -171,7 +172,9 @@ public class SpecialEvent implements ToXContentObject, Writeable { builder.dateField(START_TIME.getPreferredName(), START_TIME.getPreferredName() + "_string", startTime.toInstant().toEpochMilli()); builder.dateField(END_TIME.getPreferredName(), END_TIME.getPreferredName() + "_string", endTime.toInstant().toEpochMilli()); builder.field(JOB_IDS.getPreferredName(), jobIds); - builder.field(TYPE.getPreferredName(), SPECIAL_EVENT_TYPE); + if (params.paramAsBoolean(MlMetaIndex.INCLUDE_TYPE_KEY, false)) { + builder.field(TYPE.getPreferredName(), SPECIAL_EVENT_TYPE); + } builder.endObject(); return builder; } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/job/config/MlFilter.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/job/config/MlFilter.java index deac591efc1..874405f9bf5 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/ml/job/config/MlFilter.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/job/config/MlFilter.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.xpack.ml.MlMetaIndex; import java.io.IOException; import java.util.ArrayList; @@ -25,7 +26,6 @@ public class MlFilter implements ToXContentObject, Writeable { public static final String DOCUMENT_ID_PREFIX = "filter_"; - public static final String INCLUDE_TYPE_KEY = "include_type"; public static final String FILTER_TYPE = "filter"; public static final ParseField TYPE = new ParseField("type"); @@ -67,7 +67,7 @@ public class MlFilter implements ToXContentObject, Writeable { builder.startObject(); builder.field(ID.getPreferredName(), id); builder.field(ITEMS.getPreferredName(), items); - if (params.paramAsBoolean(INCLUDE_TYPE_KEY, false)) { + if (params.paramAsBoolean(MlMetaIndex.INCLUDE_TYPE_KEY, false)) { builder.field(TYPE.getPreferredName(), FILTER_TYPE); } builder.endObject(); diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/rest/calendar/RestDeleteCalendarAction.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/rest/calendar/RestDeleteCalendarAction.java new file mode 100644 index 00000000000..2e59fb8c514 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/rest/calendar/RestDeleteCalendarAction.java @@ -0,0 +1,39 @@ +/* + * 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.ml.rest.calendar; + +import org.elasticsearch.client.node.NodeClient; +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.ml.MachineLearning; +import org.elasticsearch.xpack.ml.action.DeleteCalendarAction; +import org.elasticsearch.xpack.ml.calendars.Calendar; + +import java.io.IOException; + +public class RestDeleteCalendarAction extends BaseRestHandler { + + public RestDeleteCalendarAction(Settings settings, RestController controller) { + super(settings); + controller.registerHandler(RestRequest.Method.DELETE, + MachineLearning.BASE_PATH + "calendars/{" + Calendar.ID.getPreferredName() + "}", this); + } + + @Override + public String getName() { + return "xpack_ml_delete_calendar_action"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException { + DeleteCalendarAction.Request request = new DeleteCalendarAction.Request(restRequest.param(Calendar.ID.getPreferredName())); + return channel -> client.execute(DeleteCalendarAction.INSTANCE, request, new AcknowledgedRestListener<>(channel)); + } + +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/rest/calendar/RestGetCalendarsAction.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/rest/calendar/RestGetCalendarsAction.java new file mode 100644 index 00000000000..449ade7d6a0 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/rest/calendar/RestGetCalendarsAction.java @@ -0,0 +1,51 @@ +/* + * 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.ml.rest.calendar; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.Strings; +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.RestStatusToXContentListener; +import org.elasticsearch.xpack.ml.MachineLearning; +import org.elasticsearch.xpack.ml.action.GetCalendarsAction; +import org.elasticsearch.xpack.ml.action.util.PageParams; +import org.elasticsearch.xpack.ml.calendars.Calendar; + +import java.io.IOException; + +public class RestGetCalendarsAction extends BaseRestHandler { + + public RestGetCalendarsAction(Settings settings, RestController controller) { + super(settings); + controller.registerHandler(RestRequest.Method.GET, MachineLearning.BASE_PATH + "calendars/{" + Calendar.ID.getPreferredName() + "}", + this); + controller.registerHandler(RestRequest.Method.GET, MachineLearning.BASE_PATH + "calendars/", this); + } + + @Override + public String getName() { + return "xpack_ml_get_calendars_action"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException { + GetCalendarsAction.Request getRequest = new GetCalendarsAction.Request(); + String calendarId = restRequest.param(Calendar.ID.getPreferredName()); + if (!Strings.isNullOrEmpty(calendarId)) { + getRequest.setCalendarId(calendarId); + } + + if (restRequest.hasParam(PageParams.FROM.getPreferredName()) || restRequest.hasParam(PageParams.SIZE.getPreferredName())) { + getRequest.setPageParams(new PageParams(restRequest.paramAsInt(PageParams.FROM.getPreferredName(), PageParams.DEFAULT_FROM), + restRequest.paramAsInt(PageParams.SIZE.getPreferredName(), PageParams.DEFAULT_SIZE))); + } + + return channel -> client.execute(GetCalendarsAction.INSTANCE, getRequest, new RestStatusToXContentListener<>(channel)); + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/rest/calendar/RestPutCalendarAction.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/rest/calendar/RestPutCalendarAction.java new file mode 100644 index 00000000000..171c1f3d801 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/rest/calendar/RestPutCalendarAction.java @@ -0,0 +1,51 @@ +/* + * 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.ml.rest.calendar; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.xpack.ml.MachineLearning; +import org.elasticsearch.xpack.ml.action.PutCalendarAction; +import org.elasticsearch.xpack.ml.calendars.Calendar; + +import java.io.IOException; +import java.util.Collections; + +public class RestPutCalendarAction extends BaseRestHandler { + + public RestPutCalendarAction(Settings settings, RestController controller) { + super(settings); + controller.registerHandler(RestRequest.Method.PUT, + MachineLearning.BASE_PATH + "calendars/{" + Calendar.ID.getPreferredName() + "}", this); + } + + @Override + public String getName() { + return "xpack_ml_put_calendar_action"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException { + String calendarId = restRequest.param(Calendar.ID.getPreferredName()); + + PutCalendarAction.Request putCalendarRequest; + // A calendar can be created with just a name or with an optional body + if (restRequest.hasContentOrSourceParam()) { + XContentParser parser = restRequest.contentOrSourceParamParser(); + putCalendarRequest = PutCalendarAction.Request.parseRequest(calendarId, parser); + } else { + putCalendarRequest = new PutCalendarAction.Request(new Calendar(calendarId, Collections.emptyList())); + } + + return channel -> client.execute(PutCalendarAction.INSTANCE, putCalendarRequest, new RestToXContentListener<>(channel)); + } +} + diff --git a/plugin/src/test/java/org/elasticsearch/xpack/ml/action/GetCalendarsActionRequestTests.java b/plugin/src/test/java/org/elasticsearch/xpack/ml/action/GetCalendarsActionRequestTests.java new file mode 100644 index 00000000000..b177f646bc9 --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/ml/action/GetCalendarsActionRequestTests.java @@ -0,0 +1,25 @@ +/* + * 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.ml.action; + +import org.elasticsearch.test.AbstractStreamableTestCase; + +public class GetCalendarsActionRequestTests extends AbstractStreamableTestCase { + + + @Override + protected GetCalendarsAction.Request createTestInstance() { + GetCalendarsAction.Request request = new GetCalendarsAction.Request(); + request.setCalendarId(randomAlphaOfLengthBetween(1, 20)); + return request; + } + + @Override + protected GetCalendarsAction.Request createBlankInstance() { + return new GetCalendarsAction.Request(); + } + +} diff --git a/plugin/src/test/java/org/elasticsearch/xpack/ml/action/PutCalendarActionRequestTests.java b/plugin/src/test/java/org/elasticsearch/xpack/ml/action/PutCalendarActionRequestTests.java new file mode 100644 index 00000000000..db9879a0ff6 --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/ml/action/PutCalendarActionRequestTests.java @@ -0,0 +1,44 @@ +/* + * 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.ml.action; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractStreamableXContentTestCase; +import org.elasticsearch.xpack.ml.calendars.Calendar; + +import java.util.ArrayList; +import java.util.List; + +public class PutCalendarActionRequestTests extends AbstractStreamableXContentTestCase { + + private final String calendarId = randomAlphaOfLengthBetween(1, 20); + + @Override + protected PutCalendarAction.Request createTestInstance() { + int size = randomInt(10); + List jobIds = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + jobIds.add(randomAlphaOfLengthBetween(1, 20)); + } + Calendar calendar = new Calendar(calendarId, jobIds); + return new PutCalendarAction.Request(calendar); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + + @Override + protected PutCalendarAction.Request createBlankInstance() { + return new PutCalendarAction.Request(); + } + + @Override + protected PutCalendarAction.Request doParseInstance(XContentParser parser) { + return PutCalendarAction.Request.parseRequest(calendarId, parser); + } +} diff --git a/plugin/src/test/java/org/elasticsearch/xpack/ml/calendars/CalendarTests.java b/plugin/src/test/java/org/elasticsearch/xpack/ml/calendars/CalendarTests.java new file mode 100644 index 00000000000..23755a0ded9 --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/ml/calendars/CalendarTests.java @@ -0,0 +1,49 @@ +/* + * 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.ml.calendars; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; + +public class CalendarTests extends AbstractSerializingTestCase { + + @Override + protected Calendar createTestInstance() { + int size = randomInt(10); + List items = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + items.add(randomAlphaOfLengthBetween(1, 20)); + } + return new Calendar(randomAlphaOfLengthBetween(1, 20), items); + } + + @Override + protected Writeable.Reader instanceReader() { + return Calendar::new; + } + + @Override + protected Calendar doParseInstance(XContentParser parser) throws IOException { + return Calendar.PARSER.apply(parser, null).build(); + } + + public void testNullId() { + NullPointerException ex = expectThrows(NullPointerException.class, () -> new Calendar(null, Collections.emptyList())); + assertEquals(Calendar.ID.getPreferredName() + " must not be null", ex.getMessage()); + } + + public void testDocumentId() { + assertThat(Calendar.documentId("foo"), equalTo("calendar_foo")); + } +} \ No newline at end of file diff --git a/plugin/src/test/java/org/elasticsearch/xpack/ml/integration/JobProviderIT.java b/plugin/src/test/java/org/elasticsearch/xpack/ml/integration/JobProviderIT.java index 4acb4afdb32..96f932c86f0 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/ml/integration/JobProviderIT.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/ml/integration/JobProviderIT.java @@ -278,7 +278,8 @@ public class JobProviderIT extends XPackSingleNodeTestCase { for (SpecialEvent event : events) { IndexRequest indexRequest = new IndexRequest(MlMetaIndex.INDEX_NAME, MlMetaIndex.TYPE, event.documentId()); try (XContentBuilder builder = XContentFactory.jsonBuilder()) { - indexRequest.source(event.toXContent(builder, ToXContent.EMPTY_PARAMS)); + ToXContent.MapParams params = new ToXContent.MapParams(Collections.singletonMap(MlMetaIndex.INCLUDE_TYPE_KEY, "true")); + indexRequest.source(event.toXContent(builder, params)); bulkRequest.add(indexRequest); } } diff --git a/plugin/src/test/resources/rest-api-spec/api/xpack.ml.delete_calendar.json b/plugin/src/test/resources/rest-api-spec/api/xpack.ml.delete_calendar.json new file mode 100644 index 00000000000..3fcfa8582e5 --- /dev/null +++ b/plugin/src/test/resources/rest-api-spec/api/xpack.ml.delete_calendar.json @@ -0,0 +1,17 @@ +{ + "xpack.ml.delete_calendar": { + "methods": [ "DELETE" ], + "url": { + "path": "/_xpack/ml/calendars/{calendar_id}", + "paths": [ "/_xpack/ml/calendars/{calendar_id}" ], + "parts": { + "calendar_id": { + "type" : "string", + "required" : true, + "description" : "The ID of the calendar to delete" + } + } + }, + "body": null + } +} diff --git a/plugin/src/test/resources/rest-api-spec/api/xpack.ml.get_calendars.json b/plugin/src/test/resources/rest-api-spec/api/xpack.ml.get_calendars.json new file mode 100644 index 00000000000..44c06e3501b --- /dev/null +++ b/plugin/src/test/resources/rest-api-spec/api/xpack.ml.get_calendars.json @@ -0,0 +1,29 @@ +{ + "xpack.ml.get_calendars": { + "methods": [ "GET" ], + "url": { + "path": "/_xpack/ml/calendars/{calendar_id}", + "paths": [ + "/_xpack/ml/calendars", + "/_xpack/ml/calendars/{calendar_id}" + ], + "parts": { + "calendar_id": { + "type": "string", + "description": "The ID of the calendar to fetch" + } + }, + "params": { + "from": { + "type": "int", + "description": "skips a number of calendars" + }, + "size": { + "type": "int", + "description": "specifies a max number of calendars to get" + } + } + }, + "body": null + } +} diff --git a/plugin/src/test/resources/rest-api-spec/api/xpack.ml.put_calendar.json b/plugin/src/test/resources/rest-api-spec/api/xpack.ml.put_calendar.json new file mode 100644 index 00000000000..d762ad29315 --- /dev/null +++ b/plugin/src/test/resources/rest-api-spec/api/xpack.ml.put_calendar.json @@ -0,0 +1,20 @@ +{ + "xpack.ml.put_calendar": { + "methods": [ "PUT" ], + "url": { + "path": "/_xpack/ml/calendars/{calendar_id}", + "paths": [ "/_xpack/ml/calendars/{calendar_id}" ], + "parts": { + "calendar_id": { + "type": "string", + "required": true, + "description": "The ID of the calendar to create" + } + } + }, + "body": { + "description" : "The calendar details", + "required" : false + } + } +} diff --git a/plugin/src/test/resources/rest-api-spec/test/ml/calendar_crud.yml b/plugin/src/test/resources/rest-api-spec/test/ml/calendar_crud.yml new file mode 100644 index 00000000000..216cb521b5d --- /dev/null +++ b/plugin/src/test/resources/rest-api-spec/test/ml/calendar_crud.yml @@ -0,0 +1,108 @@ +--- +"Test calendar CRUD": + + - do: + xpack.ml.put_calendar: + calendar_id: "advent" + body: > + { + "job_ids": ["abc", "xyz"] + } + - match: { calendar_id: advent } + - match: { job_ids.0: abc } + - match: { job_ids.1: xyz } + + - do: + xpack.ml.get_calendars: + calendar_id: "advent" + - match: { count: 1 } + - match: + calendars.0: + calendar_id: "advent" + job_ids: ["abc", "xyz"] + - is_false: type + + - do: + xpack.ml.put_calendar: + calendar_id: "Dogs of the Year" + body: > + { + "job_ids": ["abc2"] + } + + - do: + xpack.ml.put_calendar: + calendar_id: "Cats of the Year" + + - do: + xpack.ml.get_calendars: {} + - match: { count: 3 } + + - do: + xpack.ml.delete_calendar: + calendar_id: "Dogs of the Year" + + - do: + xpack.ml.get_calendars: {} + - match: { count: 2 } + + - do: + catch: missing + xpack.ml.get_calendars: + calendar_id: "Dogs of the Year" + +--- +"Test PageParams": + - do: + xpack.ml.put_calendar: + calendar_id: "Calendar1" + - do: + xpack.ml.put_calendar: + calendar_id: "Calendar2" + - do: + xpack.ml.put_calendar: + calendar_id: "Calendar3" + + - do: + xpack.ml.get_calendars: + from: 2 + - match: { count: 1 } + - match: { calendars.0.calendar_id: Calendar3 } + + - do: + xpack.ml.get_calendars: + from: 1 + size: 1 + - match: { count: 1 } + - match: { calendars.0.calendar_id: Calendar2 } + +--- +"Test PageParams with ID is invalid": + - do: + catch: bad_request + xpack.ml.get_calendars: + calendar_id: Tides + size: 10 + +--- +"Test cannot overwrite an exisiting calendar": + + - do: + xpack.ml.put_calendar: + calendar_id: "Mayan" + body: > + { + "job_ids": ["apocalypse"] + } + + - do: + catch: /version_conflict_engine_exception/ + xpack.ml.put_calendar: + calendar_id: "Mayan" + +--- +"Test cannot create calendar with name _all": + - do: + catch: bad_request + xpack.ml.put_calendar: + calendar_id: "_all" diff --git a/qa/smoke-test-ml-with-security/build.gradle b/qa/smoke-test-ml-with-security/build.gradle index d34c99af35a..c39154ea767 100644 --- a/qa/smoke-test-ml-with-security/build.gradle +++ b/qa/smoke-test-ml-with-security/build.gradle @@ -17,6 +17,8 @@ integTestRunner { systemProperty 'tests.rest.blacklist', [ // Remove tests that are expected to throw an exception, because we cannot then // know whether to expect an authorization exception or a validation exception + 'ml/calendar_crud/Test cannot create calendar with name _all', + 'ml/calendar_crud/Test PageParams with ID is invalid', 'ml/custom_all_field/Test querying custom all field', 'ml/datafeeds_crud/Test delete datafeed with missing id', 'ml/datafeeds_crud/Test put datafeed referring to missing job_id',