diff --git a/server/src/internalClusterTest/java/org/opensearch/action/admin/indices/view/ViewIT.java b/server/src/internalClusterTest/java/org/opensearch/action/admin/indices/view/ViewIT.java new file mode 100644 index 0000000000000..ac706491478d1 --- /dev/null +++ b/server/src/internalClusterTest/java/org/opensearch/action/admin/indices/view/ViewIT.java @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.action.admin.indices.view; + +import static org.hamcrest.Matchers.is; +import static org.opensearch.test.hamcrest.OpenSearchAssertions.assertHitCount; + +import java.util.List; + +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.index.IndexNotFoundException; +import org.opensearch.test.BackgroundIndexer; +import org.opensearch.test.OpenSearchIntegTestCase; +import org.opensearch.test.OpenSearchIntegTestCase.ClusterScope; +import org.opensearch.test.OpenSearchIntegTestCase.Scope; +import org.opensearch.test.junit.annotations.TestLogging; + +@ClusterScope(scope = Scope.TEST, numDataNodes = 2) +public class ViewIT extends OpenSearchIntegTestCase { + + private int createIndexWithDocs(final String indexName) throws Exception { + createIndex(indexName); + ensureGreen(indexName); + + final int numOfDocs = scaledRandomIntBetween(0, 200); + try (final BackgroundIndexer indexer = new BackgroundIndexer(indexName, "_doc", client(), numOfDocs)) { + waitForDocs(numOfDocs, indexer); + } + + refresh(indexName); + assertHitCount(client().prepareSearch(indexName).setSize(0).get(), numOfDocs); + return numOfDocs; + } + + private CreateViewAction.Response createView(final String name, final String indexPattern) throws Exception { + final CreateViewAction.Request request = new CreateViewAction.Request(name, null, List.of(new CreateViewAction.Request.Target(indexPattern))); + final CreateViewAction.Response response = client().admin().indices().createView(request).actionGet(); + performRemoteStoreTestAction(); + return response; + } + + private SearchResponse searchView(final String viewName) throws Exception { + final SearchViewAction.Request request = SearchViewAction.createRequestWith(viewName, new SearchRequest()); + final SearchResponse response = client().searchView(request).actionGet(); + return response; + } + + public void testBasicOperations() throws Exception { + final String indexInView1 = "index-1"; + final String indexInView2 = "index-2"; + final String indexNotInView = "another-index-1"; + + final int indexInView1DocCount = createIndexWithDocs(indexInView1); + final int indexInView2DocCount = createIndexWithDocs(indexInView2); + createIndexWithDocs(indexNotInView); + + logger.info("Testing view with no matches"); + createView("no-matches", "this-pattern-will-match-nothing"); + final IndexNotFoundException ex = assertThrows(IndexNotFoundException.class, () -> searchView("no-matches")); + assertThat(ex.getMessage(), is("no such index [this-pattern-will-match-nothing]")); + + logger.info("Testing view with exact index match"); + createView("only-index-1", "index-1"); + assertHitCount(searchView("only-index-1"), indexInView1DocCount); + + logger.info("Testing view with wildcard matches"); + createView("both-indices", "index-*"); + assertHitCount(searchView("both-indices"), indexInView1DocCount + indexInView2DocCount); + } +} diff --git a/server/src/main/java/org/opensearch/action/ActionModule.java b/server/src/main/java/org/opensearch/action/ActionModule.java index e2a738ac959a3..b18498de9581e 100644 --- a/server/src/main/java/org/opensearch/action/ActionModule.java +++ b/server/src/main/java/org/opensearch/action/ActionModule.java @@ -225,6 +225,7 @@ import org.opensearch.action.admin.indices.validate.query.TransportValidateQueryAction; import org.opensearch.action.admin.indices.validate.query.ValidateQueryAction; import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.action.admin.indices.view.SearchViewAction; import org.opensearch.action.bulk.BulkAction; import org.opensearch.action.bulk.TransportBulkAction; import org.opensearch.action.bulk.TransportShardBulkAction; @@ -410,6 +411,7 @@ import org.opensearch.rest.action.admin.indices.RestUpgradeAction; import org.opensearch.rest.action.admin.indices.RestUpgradeStatusAction; import org.opensearch.rest.action.admin.indices.RestValidateQueryAction; +import org.opensearch.rest.action.admin.indices.RestViewAction; import org.opensearch.rest.action.cat.AbstractCatAction; import org.opensearch.rest.action.cat.RestAliasAction; import org.opensearch.rest.action.cat.RestAllocationAction; @@ -724,6 +726,7 @@ public void reg // Views: actions.register(CreateViewAction.INSTANCE, CreateViewAction.TransportAction.class); + actions.register(SearchViewAction.INSTANCE, SearchViewAction.TransportAction.class); // Persistent tasks: actions.register(StartPersistentTaskAction.INSTANCE, StartPersistentTaskAction.TransportAction.class); @@ -919,6 +922,10 @@ public void initRestHandlers(Supplier nodesInCluster) { registerHandler.accept(new RestResolveIndexAction()); registerHandler.accept(new RestDataStreamsStatsAction()); + // View API + registerHandler.accept(new RestViewAction.CreateViewHandler()); + registerHandler.accept(new RestViewAction.SearchViewHandler()); + // CAT API registerHandler.accept(new RestAllocationAction()); registerHandler.accept(new RestCatSegmentReplicationAction()); diff --git a/server/src/main/java/org/opensearch/action/admin/indices/view/CreateViewAction.java b/server/src/main/java/org/opensearch/action/admin/indices/view/CreateViewAction.java index 10d84bd832229..f4e8b1db1956a 100644 --- a/server/src/main/java/org/opensearch/action/admin/indices/view/CreateViewAction.java +++ b/server/src/main/java/org/opensearch/action/admin/indices/view/CreateViewAction.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.opensearch.action.ActionRequestValidationException; import org.opensearch.action.ActionType; @@ -14,15 +15,22 @@ import org.opensearch.cluster.block.ClusterBlockException; import org.opensearch.cluster.block.ClusterBlockLevel; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.metadata.ViewService; +import org.opensearch.cluster.metadata.View; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.annotation.ExperimentalApi; import org.opensearch.common.inject.Inject; +import org.opensearch.core.ParseField; import org.opensearch.core.action.ActionListener; import org.opensearch.core.action.ActionResponse; import org.opensearch.core.common.Strings; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.core.common.util.CollectionUtils; +import org.opensearch.core.xcontent.ConstructingObjectParser; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; @@ -30,60 +38,34 @@ public class CreateViewAction extends ActionType { public static final CreateViewAction INSTANCE = new CreateViewAction(); - public static final String NAME = "cluster:views:create"; + public static final String NAME = "cluster:admin:views:create"; private CreateViewAction() { super(NAME, CreateViewAction.Response::new); } - - /** View target representation for create requests */ - public static class ViewTarget implements Writeable { - public final String indexPattern; - - public ViewTarget(final String indexPattern) { - this.indexPattern = indexPattern; - } - - public ViewTarget(final StreamInput in) throws IOException { - this.indexPattern = in.readString(); - } - - public String getIndexPattern() { - return indexPattern; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(indexPattern); - } - - public ActionRequestValidationException validate() { - ActionRequestValidationException validationException = null; - - if (Strings.isNullOrEmpty(indexPattern)) { - validationException = ValidateActions.addValidationError("index pattern cannot be empty or null", validationException); - } - - return validationException; - } - - } - /** * Request for Creating View */ + @ExperimentalApi public static class Request extends ClusterManagerNodeRequest { private final String name; private final String description; - private final List targets; + private final List targets; - public Request(final String name, final String description, final List targets) { + public Request(final String name, final String description, final List targets) { this.name = name; - this.description = description; + this.description = Objects.requireNonNullElse(description, ""); this.targets = targets; } + public Request(final StreamInput in) throws IOException { + super(in); + this.name = in.readString(); + this.description = in.readString(); + this.targets = in.readList(Target::new); + } + public String getName() { return name; } @@ -92,34 +74,42 @@ public String getDescription() { return description; } - public List getTargets() { + public List getTargets() { return new ArrayList<>(targets); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Request that = (Request) o; + return name.equals(that.name) + && description.equals(that.description) + && targets.equals(that.targets); + } + + @Override + public int hashCode() { + return Objects.hash(name, description, targets); + } + @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; if (Strings.isNullOrEmpty(name)) { validationException = ValidateActions.addValidationError("Name is cannot be empty or null", validationException); } - if (targets.isEmpty()) { + if (CollectionUtils.isEmpty(targets)) { validationException = ValidateActions.addValidationError("targets cannot be empty", validationException); - } - - for (final ViewTarget target : targets) { - validationException = target.validate(); + } else { + for (final Target target : targets) { + validationException = target.validate(); + } } return validationException; } - public Request(final StreamInput in) throws IOException { - super(in); - this.name = in.readString(); - this.description = in.readString(); - this.targets = in.readList(ViewTarget::new); - } - @Override public void writeTo(final StreamOutput out) throws IOException { super.writeTo(out); @@ -127,26 +117,121 @@ public void writeTo(final StreamOutput out) throws IOException { out.writeString(description); out.writeList(targets); } + + /** View target representation for create requests */ + @ExperimentalApi + public static class Target implements Writeable { + public final String indexPattern; + + public Target(final String indexPattern) { + this.indexPattern = indexPattern; + } + + public Target(final StreamInput in) throws IOException { + this.indexPattern = in.readString(); + } + + public String getIndexPattern() { + return indexPattern; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Target that = (Target) o; + return indexPattern.equals(that.indexPattern); + } + + @Override + public int hashCode() { + return Objects.hash(indexPattern); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(indexPattern); + } + + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + + if (Strings.isNullOrEmpty(indexPattern)) { + validationException = ValidateActions.addValidationError("index pattern cannot be empty or null", validationException); + } + + return validationException; + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "target", + args -> new Target((String) args[0]) + ); + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), View.Target.INDEX_PATTERN_FIELD); + } + + public static Target fromXContent(final XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + } + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "create_view_request", + args -> new Request((String) args[0], (String) args[1], (List) args[2]) + ); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), View.NAME_FIELD); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), View.DESCRIPTION_FIELD); + PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), (p, c) -> Target.fromXContent(p), View.TARGETS_FIELD); + } + + public static Request fromXContent(final XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } } - /** Response after view is created */ - public static class Response extends ActionResponse { + /** Response for view creation */ + @ExperimentalApi + public static class Response extends ActionResponse implements ToXContentObject { - private final org.opensearch.cluster.metadata.View createdView; + private final View createdView; - public Response(final org.opensearch.cluster.metadata.View createdView) { + public Response(final View createdView) { this.createdView = createdView; } public Response(final StreamInput in) throws IOException { super(in); - this.createdView = new org.opensearch.cluster.metadata.View(in); + this.createdView = new View(in); } @Override public void writeTo(final StreamOutput out) throws IOException { this.createdView.writeTo(out); } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.startObject(); + builder.field("view", createdView); + builder.endObject(); + return builder; + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "create_view_response", + args -> new Response((View) args[0]) + ); + static { + PARSER.declareObject(ConstructingObjectParser.constructorArg(), View.PARSER, new ParseField("view")); + } + + public static Response fromXContent(final XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } } /** @@ -171,22 +256,22 @@ public TransportAction( @Override protected String executor() { - return ThreadPool.Names.SAME; + return ThreadPool.Names.MANAGEMENT; } @Override - protected Response read(StreamInput in) throws IOException { + protected Response read(final StreamInput in) throws IOException { return new Response(in); } @Override - protected void clusterManagerOperation(Request request, ClusterState state, ActionListener listener) + protected void clusterManagerOperation(final Request request, final ClusterState state, final ActionListener listener) throws Exception { viewService.createView(request, listener); } @Override - protected ClusterBlockException checkBlock(Request request, ClusterState state) { + protected ClusterBlockException checkBlock(final Request request, final ClusterState state) { return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE); } } diff --git a/server/src/main/java/org/opensearch/action/admin/indices/view/SearchViewAction.java b/server/src/main/java/org/opensearch/action/admin/indices/view/SearchViewAction.java new file mode 100644 index 0000000000000..b27fb6339418b --- /dev/null +++ b/server/src/main/java/org/opensearch/action/admin/indices/view/SearchViewAction.java @@ -0,0 +1,134 @@ +package org.opensearch.action.admin.indices.view; + +import java.io.IOException; +import java.util.Objects; +import java.util.function.Function; + +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.action.ActionType; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.annotation.ExperimentalApi; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.Strings; +import org.opensearch.core.common.io.stream.BytesStreamInput; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.action.ValidateActions.addValidationError; + +/** Action to create a view */ +public class SearchViewAction extends ActionType { + + public static final SearchViewAction INSTANCE = new SearchViewAction(); + public static final String NAME = "cluster:admin:views:search"; + + private SearchViewAction() { + super(NAME, SearchResponse::new); + } + + /** Given a search request, creates a ViewSearchRequest */ + public static Request createRequestWith(final String view, final SearchRequest searchRequest) + throws IOException { + final BytesStreamOutput savedSearchRequest = new BytesStreamOutput(); + searchRequest.writeTo(savedSearchRequest); + savedSearchRequest.writeString(view); + + final BytesStreamInput input = new BytesStreamInput(savedSearchRequest.bytes().toBytesRef().bytes); + return new Request(input); + } + /** + * Wraps the functionality of search requests and tailors for what is available + * when searching through views + */ + @ExperimentalApi + public static class Request extends SearchRequest { + + private final String view; + + public Request(final String view) { + super(); + this.view = view; + } + + public Request(final StreamInput in) throws IOException { + super(in); + view = in.readString(); + } + + public String getView() { + return view; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Request that = (Request) o; + return view.equals(that.view) + && super.equals(that); + } + + @Override + public int hashCode() { + return Objects.hash(view, super.hashCode()); + } + + @Override + public ActionRequestValidationException validate() { + final Function unsupported = (String x) -> x + " is not supported when searching views"; + ActionRequestValidationException validationException = super.validate(); + + if (scroll() != null) { + validationException = addValidationError(unsupported.apply("Scroll"), validationException); + } + + // TODO: Filter out anything additional search features that are not supported + + if (Strings.isNullOrEmpty(view)) { + validationException = addValidationError("View is required", validationException); + } + + return validationException; + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(view); + } + + @Override + public String toString() { + return super.toString().replace("SearchRequest{", "SearchViewAction.Request{view=" + view + ","); + } + } + + /** + * Transport Action for searching a View + */ + public static class TransportAction extends HandledTransportAction { + + private final ViewService viewService; + + @Inject + public TransportAction( + final TransportService transportService, + final ActionFilters actionFilters, + final ViewService viewService) { + super(NAME, transportService, actionFilters, Request::new); + this.viewService = viewService; + } + + @Override + protected void doExecute(final Task task, final Request request, final ActionListener listener) { + viewService.searchView(request, listener); + } + } +} diff --git a/server/src/main/java/org/opensearch/cluster/metadata/ViewService.java b/server/src/main/java/org/opensearch/action/admin/indices/view/ViewService.java similarity index 61% rename from server/src/main/java/org/opensearch/cluster/metadata/ViewService.java rename to server/src/main/java/org/opensearch/action/admin/indices/view/ViewService.java index 4056e477f2226..4839244f8179c 100644 --- a/server/src/main/java/org/opensearch/cluster/metadata/ViewService.java +++ b/server/src/main/java/org/opensearch/action/admin/indices/view/ViewService.java @@ -1,13 +1,19 @@ -package org.opensearch.cluster.metadata; +package org.opensearch.action.admin.indices.view; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.ResourceNotFoundException; +import org.opensearch.action.search.SearchAction; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.node.NodeClient; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.ClusterStateUpdateTask; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.metadata.View; import org.opensearch.cluster.service.ClusterService; import org.opensearch.core.action.ActionListener; @@ -16,9 +22,11 @@ public class ViewService { private final static Logger LOG = LogManager.getLogger(ViewService.class); private final ClusterService clusterService; + private final NodeClient client; - public ViewService(final ClusterService clusterService) { + public ViewService(final ClusterService clusterService, NodeClient client) { this.clusterService = clusterService; + this.client = client; } public void createView(final CreateViewAction.Request request, final ActionListener listener) { @@ -51,4 +59,25 @@ public void clusterStateProcessed(final String source, final ClusterState oldSta } }); } + + public void searchView(final SearchViewAction.Request request, final ActionListener listener) { + final Optional optView = Optional.ofNullable(clusterService) + .map(ClusterService::state) + .map(ClusterState::metadata) + .map(m -> m.views()) + .map(views -> views.get(request.getView())); + + if (optView.isEmpty()) { + throw new ResourceNotFoundException("no such view [" + request.getView() + "]"); + } + final View view = optView.get(); + + final String[] indices = view.getTargets().stream() + .map(View.Target::getIndexPattern) + .collect(Collectors.toList()) + .toArray(new String[0]); + request.indices(indices); + + client.executeLocally(SearchAction.INSTANCE, request, listener); + } } diff --git a/server/src/main/java/org/opensearch/action/search/ViewSearchRequest.java b/server/src/main/java/org/opensearch/action/search/ViewSearchRequest.java deleted file mode 100644 index d457039f2a0fe..0000000000000 --- a/server/src/main/java/org/opensearch/action/search/ViewSearchRequest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.action.search; - -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.cluster.metadata.View; -import org.opensearch.common.annotation.ExperimentalApi; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import java.io.IOException; -import java.util.Objects; -import java.util.function.Function; - -import static org.opensearch.action.ValidateActions.addValidationError; - -/** Wraps the functionality of search requests and tailors for what is available when searching through views - */ -@ExperimentalApi -public class ViewSearchRequest extends SearchRequest { - - public final View view; - - public ViewSearchRequest(final View view) { - super(); - this.view = view; - } - - public ViewSearchRequest(final StreamInput in) throws IOException { - super(in); - view = new View(in); - } - - @Override - public ActionRequestValidationException validate() { - final Function unsupported = (String x) -> x + " is not supported when searching views"; - ActionRequestValidationException validationException = super.validate(); - - if (scroll() != null) { - validationException = addValidationError(unsupported.apply("Scroll"), validationException); - } - - // TODO: Filter out anything additional search features that are not supported - - return validationException; - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - super.writeTo(out); - view.writeTo(out); - } - - @Override - public boolean equals(final Object o) { - return this.hashCode() == o.hashCode(); - } - - @Override - public int hashCode() { - return Objects.hash(view, super.hashCode()); - } - - @Override - public String toString() { - return super.toString().replace("SearchRequest{", "ViewSearchRequest{view=" + view + ","); - } -} diff --git a/server/src/main/java/org/opensearch/client/IndicesAdminClient.java b/server/src/main/java/org/opensearch/client/IndicesAdminClient.java index 20dab1caa36c4..1e27073e0450d 100644 --- a/server/src/main/java/org/opensearch/client/IndicesAdminClient.java +++ b/server/src/main/java/org/opensearch/client/IndicesAdminClient.java @@ -125,6 +125,9 @@ import org.opensearch.action.admin.indices.validate.query.ValidateQueryRequest; import org.opensearch.action.admin.indices.validate.query.ValidateQueryRequestBuilder; import org.opensearch.action.admin.indices.validate.query.ValidateQueryResponse; +import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.action.admin.indices.view.SearchViewAction; +import org.opensearch.action.search.SearchResponse; import org.opensearch.action.support.master.AcknowledgedResponse; import org.opensearch.cluster.metadata.IndexMetadata.APIBlock; import org.opensearch.common.Nullable; @@ -838,4 +841,16 @@ public interface IndicesAdminClient extends OpenSearchClient { * Resolves names and wildcard expressions to indices, aliases, and data streams */ ActionFuture resolveIndex(ResolveIndexAction.Request request); + + /** Create a view */ + void createView(CreateViewAction.Request request, ActionListener listener); + + /** Create a view */ + ActionFuture createView(CreateViewAction.Request request); + + /** Search a view */ + void searchView(final SearchViewAction.Request request, final ActionListener listener); + + /** Search a view */ + ActionFuture searchView(final SearchViewAction.Request request); } diff --git a/server/src/main/java/org/opensearch/client/support/AbstractClient.java b/server/src/main/java/org/opensearch/client/support/AbstractClient.java index 786bfa38bb19c..c554316389953 100644 --- a/server/src/main/java/org/opensearch/client/support/AbstractClient.java +++ b/server/src/main/java/org/opensearch/client/support/AbstractClient.java @@ -312,6 +312,8 @@ import org.opensearch.action.admin.indices.validate.query.ValidateQueryRequest; import org.opensearch.action.admin.indices.validate.query.ValidateQueryRequestBuilder; import org.opensearch.action.admin.indices.validate.query.ValidateQueryResponse; +import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.action.admin.indices.view.SearchViewAction; import org.opensearch.action.bulk.BulkAction; import org.opensearch.action.bulk.BulkRequest; import org.opensearch.action.bulk.BulkRequestBuilder; @@ -2070,6 +2072,26 @@ public void resolveIndex(ResolveIndexAction.Request request, ActionListener resolveIndex(ResolveIndexAction.Request request) { return execute(ResolveIndexAction.INSTANCE, request); } + + @Override + public void createView(CreateViewAction.Request request, ActionListener listener) { + execute(CreateViewAction.INSTANCE, request); + } + + @Override + public ActionFuture createView(CreateViewAction.Request request) { + return execute(CreateViewAction.INSTANCE, request); + } + + @Override + public void searchView(SearchViewAction.Request request, ActionListener listener) { + execute(SearchViewAction.INSTANCE, request); + } + + @Override + public ActionFuture searchView(SearchViewAction.Request request) { + return execute(SearchViewAction.INSTANCE, request); + } } @Override diff --git a/server/src/main/java/org/opensearch/cluster/metadata/Metadata.java b/server/src/main/java/org/opensearch/cluster/metadata/Metadata.java index 69e49e7aec6eb..59dc86ea28ed6 100644 --- a/server/src/main/java/org/opensearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/opensearch/cluster/metadata/Metadata.java @@ -1348,7 +1348,7 @@ public Builder views(final Map views) { public Builder put(final View view) { Objects.requireNonNull(view, "view cannot be null"); final var replacementViews = new HashMap<>(getViews()); - replacementViews.put(view.name, view); + replacementViews.put(view.getName(), view); return views(replacementViews); } diff --git a/server/src/main/java/org/opensearch/cluster/metadata/View.java b/server/src/main/java/org/opensearch/cluster/metadata/View.java index 8db65c6afaebe..80efd8172b797 100644 --- a/server/src/main/java/org/opensearch/cluster/metadata/View.java +++ b/server/src/main/java/org/opensearch/cluster/metadata/View.java @@ -24,15 +24,15 @@ import java.util.List; import java.util.Objects; -/** TODO */ +/** View of data in OpenSearch indices */ @ExperimentalApi public class View extends AbstractDiffable implements ToXContentObject { - public final String name; - public final String description; - public final long createdAt; - public final long modifiedAt; - public final List targets; + private final String name; + private final String description; + private final long createdAt; + private final long modifiedAt; + private final List targets; public View(final String name, final String description, final Long createdAt, final Long modifiedAt, final List targets) { this.name = Objects.requireNonNull(name, "Name must be provided"); @@ -46,15 +46,53 @@ public View(final StreamInput in) throws IOException { this(in.readString(), in.readOptionalString(), in.readVLong(), in.readVLong(), in.readList(Target::new)); } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public long getCreatedAt() { + return createdAt; + } + + public long getModifiedAt() { + return modifiedAt; + } + + public List getTargets() { + return targets; + } + public static Diff readDiffFrom(final StreamInput in) throws IOException { return readDiffFrom(View::new, in); } - /** TODO */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + View that = (View) o; + return name.equals(that.name) + && description.equals(that.description) + && createdAt == that.createdAt + && modifiedAt == that.modifiedAt + && targets.equals(that.targets); + } + + @Override + public int hashCode() { + return Objects.hash(name, description, createdAt, modifiedAt, targets); + } + + /** The source of data used to project the view */ @ExperimentalApi public static class Target implements Writeable, ToXContentObject { - public final String indexPattern; + private final String indexPattern; public Target(final String indexPattern) { this.indexPattern = Objects.requireNonNull(indexPattern, "IndexPattern is required"); @@ -64,7 +102,24 @@ public Target(final StreamInput in) throws IOException { this(in.readString()); } - private static final ParseField INDEX_PATTERN_FIELD = new ParseField("indexPattern"); + public String getIndexPattern() { + return indexPattern; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Target that = (Target) o; + return indexPattern.equals(that.indexPattern); + } + + @Override + public int hashCode() { + return Objects.hash(indexPattern); + } + + public static final ParseField INDEX_PATTERN_FIELD = new ParseField("indexPattern"); @Override public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { @@ -74,16 +129,16 @@ public XContentBuilder toXContent(final XContentBuilder builder, final Params pa return builder; } - private static final ConstructingObjectParser T_PARSER = new ConstructingObjectParser<>( + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "target", args -> new Target((String) args[0]) ); static { - T_PARSER.declareString(ConstructingObjectParser.constructorArg(), INDEX_PATTERN_FIELD); + PARSER.declareString(ConstructingObjectParser.constructorArg(), INDEX_PATTERN_FIELD); } public static Target fromXContent(final XContentParser parser) throws IOException { - return T_PARSER.parse(parser, null); + return PARSER.parse(parser, null); } @Override @@ -92,14 +147,14 @@ public void writeTo(final StreamOutput out) throws IOException { } } - private static final ParseField NAME_FIELD = new ParseField("name"); - private static final ParseField DESCRIPTION_FIELD = new ParseField("description"); - private static final ParseField CREATED_AT_FIELD = new ParseField("createdAt"); - private static final ParseField MODIFIED_AT_FIELD = new ParseField("modifiedAt"); - private static final ParseField TARGETS_FIELD = new ParseField("targets"); + public static final ParseField NAME_FIELD = new ParseField("name"); + public static final ParseField DESCRIPTION_FIELD = new ParseField("description"); + public static final ParseField CREATED_AT_FIELD = new ParseField("createdAt"); + public static final ParseField MODIFIED_AT_FIELD = new ParseField("modifiedAt"); + public static final ParseField TARGETS_FIELD = new ParseField("targets"); @SuppressWarnings("unchecked") - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "view", args -> new View((String) args[0], (String) args[1], (Long) args[2], (Long) args[3], (List) args[4]) ); diff --git a/server/src/main/java/org/opensearch/cluster/metadata/ViewMetadata.java b/server/src/main/java/org/opensearch/cluster/metadata/ViewMetadata.java index 78f9707f4b536..1f9311b714d1b 100644 --- a/server/src/main/java/org/opensearch/cluster/metadata/ViewMetadata.java +++ b/server/src/main/java/org/opensearch/cluster/metadata/ViewMetadata.java @@ -29,6 +29,7 @@ import static org.opensearch.cluster.metadata.ComposableIndexTemplateMetadata.MINIMMAL_SUPPORTED_VERSION; +/** View metadata */ public class ViewMetadata implements Metadata.Custom { public static final String TYPE = "view"; @@ -141,7 +142,7 @@ public static class Builder { private final Map views = new HashMap<>(); public Builder putDataStream(final View view) { - views.put(view.name, view); + views.put(view.getName(), view); return this; } diff --git a/server/src/main/java/org/opensearch/node/Node.java b/server/src/main/java/org/opensearch/node/Node.java index d26a7deae9e77..287c674e9246e 100644 --- a/server/src/main/java/org/opensearch/node/Node.java +++ b/server/src/main/java/org/opensearch/node/Node.java @@ -44,6 +44,7 @@ import org.opensearch.action.ActionModule.DynamicActionRegistry; import org.opensearch.action.ActionType; import org.opensearch.action.admin.cluster.snapshots.status.TransportNodesSnapshotsStatus; +import org.opensearch.action.admin.indices.view.ViewService; import org.opensearch.action.search.SearchExecutionStatsCollector; import org.opensearch.action.search.SearchPhaseController; import org.opensearch.action.search.SearchRequestSlowLog; @@ -72,7 +73,6 @@ import org.opensearch.cluster.metadata.MetadataIndexUpgradeService; import org.opensearch.cluster.metadata.SystemIndexMetadataUpgradeService; import org.opensearch.cluster.metadata.TemplateUpgradeService; -import org.opensearch.cluster.metadata.ViewService; import org.opensearch.cluster.node.DiscoveryNode; import org.opensearch.cluster.node.DiscoveryNodeRole; import org.opensearch.cluster.routing.BatchedRerouteService; @@ -204,8 +204,6 @@ import org.opensearch.repositories.RepositoriesModule; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; -import org.opensearch.rest.action.admin.indices.RestViewAction; -import org.opensearch.rest.action.admin.indices.RestViewSearchAction; import org.opensearch.script.ScriptContext; import org.opensearch.script.ScriptEngine; import org.opensearch.script.ScriptModule; @@ -864,7 +862,8 @@ protected Node( ); final ViewService viewService = new ViewService( - clusterService + clusterService, + client ); Collection pluginComponents = pluginsService.filterPlugins(Plugin.class) diff --git a/server/src/main/java/org/opensearch/rest/action/admin/indices/RestViewAction.java b/server/src/main/java/org/opensearch/rest/action/admin/indices/RestViewAction.java index 44ace1fcb1f6f..bfc97c309155e 100644 --- a/server/src/main/java/org/opensearch/rest/action/admin/indices/RestViewAction.java +++ b/server/src/main/java/org/opensearch/rest/action/admin/indices/RestViewAction.java @@ -11,12 +11,17 @@ import joptsimple.internal.Strings; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.action.admin.indices.view.SearchViewAction; +import org.opensearch.action.search.SearchAction; import org.opensearch.client.node.NodeClient; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.ClusterStateUpdateTask; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.cluster.metadata.View; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.ValidationException; import org.opensearch.common.inject.Inject; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.XContentBuilder; @@ -27,10 +32,15 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestResponse; +import org.opensearch.rest.action.RestCancellableNodeClient; +import org.opensearch.rest.action.RestStatusToXContentListener; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.rest.action.search.RestSearchAction; import java.io.IOException; import java.util.List; import java.util.Optional; +import java.util.function.IntConsumer; import java.util.stream.Collectors; import static org.opensearch.rest.RestRequest.Method.DELETE; @@ -38,142 +48,153 @@ import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.rest.RestRequest.Method.PUT; -/** TODO */ -public class RestViewAction extends BaseRestHandler { +/** All rest handlers for view actions */ +public class RestViewAction { private final static Logger LOG = LogManager.getLogger(RestViewAction.class); public static final String VIEW_ID = "view_id"; + public static final String VIEW_ID_PARAMETER = "{" + VIEW_ID + "}"; - private final ClusterService clusterService; + /** Handler for create view */ + public static class CreateViewHandler extends BaseRestHandler { - @Inject - public RestViewAction(final ClusterService clusterService) { - this.clusterService = clusterService; - } - - @Override - public List routes() { - final String viewIdParameter = "{" + VIEW_ID + "}"; - - return List.of( - new NamedRoute.Builder().path("/views").method(GET).uniqueName("cluster:views:list").build(), - new NamedRoute.Builder().path("/views").method(POST).uniqueName("cluster:views:create").build(), - new NamedRoute.Builder().path("/views/" + viewIdParameter).method(GET).uniqueName("cluster:views:get").build(), - new NamedRoute.Builder().path("/views/" + viewIdParameter).method(DELETE).uniqueName("cluster:views:delete").build(), - new NamedRoute.Builder().path("/views/" + viewIdParameter).method(PUT).uniqueName("cluster:views:update").build() - ); - } - - @Override - public String getName() { - return "view_actions"; - } - - @Override - public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { - if (!request.hasParam(VIEW_ID)) { - if (request.method() == RestRequest.Method.GET) { - return channel -> channel.sendResponse(handleGet(request, channel.newBuilder())); - } - - if (request.method() == RestRequest.Method.POST) { - return channel -> handlePost(request, channel); - } - - } else if (request.hasParam(VIEW_ID)) { - if (request.method() == RestRequest.Method.GET) { - return channel -> channel.sendResponse(handleSingleGet(request, channel.newBuilder())); - } - - if (request.method() == RestRequest.Method.PUT) { - return channel -> handleSinglePut(request); - } - - if (request.method() == RestRequest.Method.DELETE) { - return channel -> handleSingleDelete(request); - } + @Override + public List routes() { + return List.of(new NamedRoute.Builder().path("/views").method(POST).uniqueName(CreateViewAction.NAME).build()); } - return channel -> channel.sendResponse( - new BytesRestResponse(RestStatus.BAD_REQUEST, "Unable to process " + request.method() + " on this endpoint " + request.path()) - ); - } - - public RestResponse handleGet(final RestRequest r, final XContentBuilder builder) throws IOException { - final List views = Optional.ofNullable(clusterService.state().getMetadata()) - .map(m -> m.views()) - .map(v -> v.values()) - .map(v -> v.stream().collect(Collectors.toList())) - .orElse(List.of()); - - return new BytesRestResponse(RestStatus.OK, builder.startObject().field("views", views).endObject()); - } - - public RestResponse handlePost(final RestRequest r, final RestChannel channel) throws IOException { - final View inputView; - try (final XContentParser parser = r.contentParser()) { - inputView = View.fromXContent(parser); + @Override + public String getName() { + return CreateViewAction.NAME; } - final long currentTime = System.currentTimeMillis(); - final View view = new View(inputView.name, inputView.description, currentTime, currentTime, inputView.targets); - - clusterService.submitStateUpdateTask("create_view_task", new ClusterStateUpdateTask() { - @Override - public ClusterState execute(final ClusterState currentState) throws Exception { - return new ClusterState.Builder(clusterService.state()).metadata(Metadata.builder(currentState.metadata()).put(view)) - .build(); - } - - @Override - public void onFailure(final String source, final Exception e) { - LOG.error("Unable to create view, due to {}", source, e); - channel.sendResponse( - new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "Unknown error occurred, see the log for details.") - ); - } - - @Override - public void clusterStateProcessed(final String source, final ClusterState oldState, final ClusterState newState) { - try { - channel.sendResponse( - new BytesRestResponse(RestStatus.CREATED, channel.newBuilder().startObject().field(view.name, view).endObject()) - ); - } catch (final IOException e) { - // TODO? - LOG.error(e); - } + @Override + protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { + try (final XContentParser parser = request.contentParser()) { + final CreateViewAction.Request createViewAction = CreateViewAction.Request.fromXContent(parser); + return channel -> client.admin().indices().createView(createViewAction, new RestToXContentListener<>(channel)); } - }); - // TODO: Handle CREATED vs UPDATED - return null; + } } - public RestResponse handleSingleGet(final RestRequest r, final XContentBuilder builder) throws IOException { - final String viewId = r.param(VIEW_ID); - - if (Strings.isNullOrEmpty(viewId)) { - return new BytesRestResponse(RestStatus.NOT_FOUND, ""); + public static class SearchViewHandler extends BaseRestHandler { + @Override + public List routes() { + return List.of( + new NamedRoute.Builder().path("/views/" + VIEW_ID_PARAMETER + "/_search").method(GET).uniqueName("cluster:views:search").build(), + new NamedRoute.Builder().path("/views/" + VIEW_ID_PARAMETER + "/_search").method(POST).uniqueName("cluster:views:search").build() + ); } - final Optional view = Optional.ofNullable(clusterService.state().getMetadata()) - .map(m -> m.views()) - .map(views -> views.get(viewId)); - - if (view.isEmpty()) { - return new BytesRestResponse(RestStatus.NOT_FOUND, ""); + @Override + public String getName() { + return "view_search_action"; } - return new BytesRestResponse(RestStatus.OK, builder.startObject().value(view).endObject()); - } + @Override + public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { + final String viewId = request.param(VIEW_ID); + + final SearchViewAction.Request viewSearchRequest = new SearchViewAction.Request(viewId); + final IntConsumer setSize = size -> viewSearchRequest.source().size(size); + + request.withContentOrSourceParamParserOrNull( + parser -> RestSearchAction.parseSearchRequest( + viewSearchRequest, + request, + parser, + client.getNamedWriteableRegistry(), + setSize + ) + ); + + final ValidationException validationResult = viewSearchRequest.validate(); + if (validationResult != null) { + throw validationResult; + } - public RestResponse handleSinglePut(final RestRequest r) { - return new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, ""); + return channel -> { + final RestCancellableNodeClient cancelClient = new RestCancellableNodeClient(client, request.getHttpChannel()); + cancelClient.execute(SearchAction.INSTANCE, viewSearchRequest, new RestStatusToXContentListener<>(channel)); + }; + } } - public RestResponse handleSingleDelete(final RestRequest r) { - return new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, ""); - } + // public List routes() { + + // return List.of( + // new NamedRoute.Builder().path("/views").method(GET).uniqueName("cluster:views:list").build(), + // new NamedRoute.Builder().path("/views/" + viewIdParameter).method(GET).uniqueName("cluster:views:get").build(), + // new NamedRoute.Builder().path("/views/" + viewIdParameter).method(DELETE).uniqueName("cluster:views:delete").build(), + // new NamedRoute.Builder().path("/views/" + viewIdParameter).method(PUT).uniqueName("cluster:views:update").build() + // ); + // } + + // public RestResponse handlePost(final RestRequest r, final RestChannel channel) throws IOException { + // final View inputView; + // try (final XContentParser parser = r.contentParser()) { + // inputView = View.fromXContent(parser); + // } + + // final long currentTime = System.currentTimeMillis(); + // final View view = new View(inputView.name, inputView.description, currentTime, currentTime, inputView.targets); + + // clusterService.submitStateUpdateTask("create_view_task", new ClusterStateUpdateTask() { + // @Override + // public ClusterState execute(final ClusterState currentState) throws Exception { + // return new ClusterState.Builder(clusterService.state()).metadata(Metadata.builder(currentState.metadata()).put(view)) + // .build(); + // } + + // @Override + // public void onFailure(final String source, final Exception e) { + // LOG.error("Unable to create view, due to {}", source, e); + // channel.sendResponse( + // new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "Unknown error occurred, see the log for details.") + // ); + // } + + // @Override + // public void clusterStateProcessed(final String source, final ClusterState oldState, final ClusterState newState) { + // try { + // channel.sendResponse( + // new BytesRestResponse(RestStatus.CREATED, channel.newBuilder().startObject().field(view.name, view).endObject()) + // ); + // } catch (final IOException e) { + // // TODO? + // LOG.error(e); + // } + // } + // }); + // // TODO: Handle CREATED vs UPDATED + // return null; + // } + + // public RestResponse handleSingleGet(final RestRequest r, final XContentBuilder builder) throws IOException { + // final String viewId = r.param(VIEW_ID); + + // if (Strings.isNullOrEmpty(viewId)) { + // return new BytesRestResponse(RestStatus.NOT_FOUND, ""); + // } + + // final Optional view = Optional.ofNullable(clusterService.state().getMetadata()) + // .map(m -> m.views()) + // .map(views -> views.get(viewId)); + + // if (view.isEmpty()) { + // return new BytesRestResponse(RestStatus.NOT_FOUND, ""); + // } + + // return new BytesRestResponse(RestStatus.OK, builder.startObject().value(view).endObject()); + // } + + // public RestResponse handleSinglePut(final RestRequest r) { + // return new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, ""); + // } + + // public RestResponse handleSingleDelete(final RestRequest r) { + // return new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, ""); + // } } diff --git a/server/src/main/java/org/opensearch/rest/action/admin/indices/RestViewSearchAction.java b/server/src/main/java/org/opensearch/rest/action/admin/indices/RestViewSearchAction.java deleted file mode 100644 index bdda2971ac38a..0000000000000 --- a/server/src/main/java/org/opensearch/rest/action/admin/indices/RestViewSearchAction.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.rest.action.admin.indices; - -import joptsimple.internal.Strings; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.search.SearchAction; -import org.opensearch.action.search.ViewSearchRequest; -import org.opensearch.client.node.NodeClient; -import org.opensearch.cluster.metadata.View; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.rest.RestStatus; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.NamedRoute; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestCancellableNodeClient; -import org.opensearch.rest.action.RestStatusToXContentListener; -import org.opensearch.rest.action.search.RestSearchAction; - -import java.io.IOException; -import java.util.List; -import java.util.Optional; -import java.util.function.IntConsumer; -import java.util.stream.Collectors; - -import static org.opensearch.rest.RestRequest.Method.GET; -import static org.opensearch.rest.RestRequest.Method.POST; - -/** TODO */ -public class RestViewSearchAction extends BaseRestHandler { - - private final static Logger LOG = LogManager.getLogger(RestViewSearchAction.class); - - private static final String VIEW_ID = "view_id"; - - private final ClusterService clusterService; - - @Inject - public RestViewSearchAction(final ClusterService clusterService) { - this.clusterService = clusterService; - } - - @Override - public List routes() { - final String viewIdParameter = "{" + VIEW_ID + "}"; - - return List.of( - new NamedRoute.Builder().path("/views/" + viewIdParameter + "/_search").method(GET).uniqueName("cluster:views:search").build(), - new NamedRoute.Builder().path("/views/" + viewIdParameter + "/_search").method(POST).uniqueName("cluster:views:search").build() - ); - } - - @Override - public String getName() { - return "view_search_action"; - } - - @Override - public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { - final String viewId = request.param(VIEW_ID); - return channel -> { - - if (Strings.isNullOrEmpty(viewId)) { - channel.sendResponse(new BytesRestResponse(RestStatus.NOT_FOUND, "")); - } - - final Optional optView = Optional.ofNullable(clusterService.state().getMetadata()) - .map(m -> m.views()) - .map(views -> views.get(viewId)); - - if (optView.isEmpty()) { - channel.sendResponse(new BytesRestResponse(RestStatus.NOT_FOUND, "")); - } - final View view = optView.get(); - - final ViewSearchRequest viewSearchRequest = new ViewSearchRequest(view); - final IntConsumer setSize = size -> viewSearchRequest.source().size(size); - - request.withContentOrSourceParamParserOrNull( - parser -> RestSearchAction.parseSearchRequest( - viewSearchRequest, - request, - parser, - client.getNamedWriteableRegistry(), - setSize - ) - ); - - // TODO: Only allow operations that are supported - - final String[] indices = view.targets.stream() - .map(target -> target.indexPattern) - .collect(Collectors.toList()) - .toArray(new String[0]); - viewSearchRequest.indices(indices); - - // TODO: Look into resource leak on cancelClient? Note; is already leaking in - // server/src/main/java/org/opensearch/rest/action/search/RestSearchAction.java - final RestCancellableNodeClient cancelClient = new RestCancellableNodeClient(client, request.getHttpChannel()); - cancelClient.execute(SearchAction.INSTANCE, viewSearchRequest, new RestStatusToXContentListener<>(channel)); - }; - } -} diff --git a/server/src/test/java/org/opensearch/action/admin/indices/view/CreateViewTests.java b/server/src/test/java/org/opensearch/action/admin/indices/view/CreateViewTests.java new file mode 100644 index 0000000000000..146b6e68ddef6 --- /dev/null +++ b/server/src/test/java/org/opensearch/action/admin/indices/view/CreateViewTests.java @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.action.admin.indices.view; + +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.test.AbstractWireSerializingTestCase; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +import java.util.List; + +public class CreateViewTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return CreateViewAction.Request::new; + } + + @Override + protected CreateViewAction.Request createTestInstance() { + return new CreateViewAction.Request(randomAlphaOfLength(8), randomAlphaOfLength(8), + randomList(5, () -> new CreateViewAction.Request.Target(randomAlphaOfLength(8)))); + } + + public void testValidateRequest() { + final CreateViewAction.Request request = new CreateViewAction.Request("my-view", "this is a description", + List.of(new CreateViewAction.Request.Target("my-indices-*"))); + assertNull(request.validate()); + } + + public void testValidateRequestWithoutName() { + final CreateViewAction.Request request = new CreateViewAction.Request("", null, null); + ActionRequestValidationException e = request.validate(); + assertNotNull(e); + assertThat(e.validationErrors().size(), equalTo(1)); + assertThat(e.validationErrors().get(0), containsString("name is missing")); + } + +} diff --git a/server/src/test/java/org/opensearch/action/admin/indices/view/SearchViewTests.java b/server/src/test/java/org/opensearch/action/admin/indices/view/SearchViewTests.java new file mode 100644 index 0000000000000..2e2eb6a2ab5d2 --- /dev/null +++ b/server/src/test/java/org/opensearch/action/admin/indices/view/SearchViewTests.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.action.admin.indices.view; + +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.test.AbstractWireSerializingTestCase; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +import java.io.IOException; +import java.util.List; + +public class SearchViewTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return SearchViewAction.Request::new; + } + + @Override + protected SearchViewAction.Request createTestInstance() { + try { + return SearchViewAction.createRequestWith(randomAlphaOfLength(8), new SearchRequest()); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + + public void testValidateRequest() throws IOException { + final SearchViewAction.Request request = SearchViewAction.createRequestWith("my-view", new SearchRequest()); + assertNull(request.validate()); + } + + public void testValidateRequestWithoutName() { + final SearchViewAction.Request request = new SearchViewAction.Request((String)null); + ActionRequestValidationException e = request.validate(); + assertNotNull(e); + assertThat(e.validationErrors().size(), equalTo(1)); + assertThat(e.validationErrors().get(0), containsString("View is required")); + } + +} diff --git a/server/src/test/java/org/opensearch/cluster/metadata/ViewTests.java b/server/src/test/java/org/opensearch/cluster/metadata/ViewTests.java index c1184ddeca915..9dc57f519aa43 100644 --- a/server/src/test/java/org/opensearch/cluster/metadata/ViewTests.java +++ b/server/src/test/java/org/opensearch/cluster/metadata/ViewTests.java @@ -14,19 +14,13 @@ package org.opensearch.cluster.metadata; import org.opensearch.cluster.metadata.View.Target; -import org.opensearch.common.UUIDs; import org.opensearch.core.common.io.stream.Writeable; -import org.opensearch.core.index.Index; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.test.AbstractSerializingTestCase; import java.io.IOException; -import java.util.ArrayList; import java.util.List; -import java.util.Locale; -import static org.opensearch.cluster.DataStreamTestHelper.createTimestampField; -import static org.opensearch.cluster.metadata.DataStream.getDefaultBackingIndexName; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; @@ -35,17 +29,13 @@ public class ViewTests extends AbstractSerializingTestCase { private static List randomTargets() { int numTargets = randomIntBetween(0, 128); - List targets = new ArrayList<>(numTargets); - for (int i = 0; i < numTargets; i++) { - targets.add(new Target(randomAlphaOfLength(10).toLowerCase(Locale.ROOT)); - } - return targets; + return randomList(numTargets, () -> new View.Target(randomAlphaOfLength(8))); } private static View randomInstance() { final List targets = randomTargets(); - final String viewName = randomAlphaOfLength(10).toLowerCase(Locale.ROOT); - final String description = randomAlphaOfLength(100).toLowerCase(Locale.ROOT); + final String viewName = randomAlphaOfLength(10); + final String description = randomAlphaOfLength(100); return new View(viewName, description, Math.abs(randomLong()), Math.abs(randomLong()), targets); } @@ -77,7 +67,7 @@ public void testNullTargets() { } public void testNullTargetIndexPattern() { - final NullPointerException npe = assertThrows(NullPointerException.class, () -> new View.Target(null)); + final NullPointerException npe = assertThrows(NullPointerException.class, () -> new View.Target((String)null)); assertThat(npe.getMessage(), equalTo("IndexPattern is required")); } @@ -85,10 +75,10 @@ public void testNullTargetIndexPattern() { public void testDefaultValues() { final View view = new View("myName", null, null, null, List.of()); - assertThat(view.name, equalTo("myName")); - assertThat(view.description, equalTo(null)); - assertThat(view.createdAt, equalTo(-1L)); - assertThat(view.modifiedAt, equalTo(-1L)); - assertThat(view.targets, empty()); + assertThat(view.getName(), equalTo("myName")); + assertThat(view.getDescription(), equalTo(null)); + assertThat(view.getCreatedAt(), equalTo(-1L)); + assertThat(view.getModifiedAt(), equalTo(-1L)); + assertThat(view.getTargets(), empty()); } }