From 27612c86e70bf6a44398fd80904758c6a8461f48 Mon Sep 17 00:00:00 2001 From: Daniel Widdis Date: Thu, 8 Sep 2022 09:44:28 -0700 Subject: [PATCH] Handle named wildcards (REST path parameters) (#123) * Register REST paths including named wildcards Signed-off-by: Daniel Widdis * Move HelloWorld extension to subpackage Signed-off-by: Daniel Widdis * Update Hello World example with named wildcard example Signed-off-by: Daniel Widdis * Pass consumed params in RestResponse header Signed-off-by: Daniel Widdis * Linelint hates me and web tool exports Signed-off-by: Daniel Widdis * Code Review tweaks Signed-off-by: Daniel Widdis * Update DESIGN.md Co-authored-by: Sarat Vemulapalli Signed-off-by: Daniel Widdis Signed-off-by: Daniel Widdis Co-authored-by: Sarat Vemulapalli --- DESIGN.md | 16 +-- DEVELOPER_GUIDE.md | 28 +++-- Docs/ExtensionRestActions.svg | 2 +- build.gradle | 16 ++- .../opensearch/sdk/ExtensionRestHandler.java | 6 +- .../sdk/ExtensionRestPathRegistry.java | 70 +++++++++++ .../opensearch/sdk/ExtensionRestResponse.java | 93 +++++++++++++++ .../org/opensearch/sdk/ExtensionsRunner.java | 17 ++- .../{ => helloworld}/HelloWorldExtension.java | 6 +- .../helloworld/rest/RestHelloAction.java | 65 ++++++++++ .../sdk/sample/helloworld/spec/openapi.json | 89 ++++++++++++++ .../sdk/sample/helloworld/spec/openapi.yaml | 55 +++++++++ .../sdk/sample/rest/RestHelloAction.java | 46 -------- ...n-settings.yml => helloworld-settings.yml} | 0 .../sdk/TestExtensionRestPathRegistry.java | 99 ++++++++++++++++ .../sdk/TestExtensionRestResponse.java | 111 ++++++++++++++++++ .../opensearch/sdk/TestExtensionsRunner.java | 2 +- .../TestHelloWorldExtension.java | 7 +- .../rest/TestRestHelloAction.java | 28 ++++- 19 files changed, 659 insertions(+), 97 deletions(-) create mode 100644 src/main/java/org/opensearch/sdk/ExtensionRestPathRegistry.java create mode 100644 src/main/java/org/opensearch/sdk/ExtensionRestResponse.java rename src/main/java/org/opensearch/sdk/sample/{ => helloworld}/HelloWorldExtension.java (95%) create mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java create mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json create mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml delete mode 100644 src/main/java/org/opensearch/sdk/sample/rest/RestHelloAction.java rename src/main/resources/sample/{extension-settings.yml => helloworld-settings.yml} (100%) create mode 100644 src/test/java/org/opensearch/sdk/TestExtensionRestPathRegistry.java create mode 100644 src/test/java/org/opensearch/sdk/TestExtensionRestResponse.java rename src/test/java/org/opensearch/sdk/sample/{ => helloworld}/TestHelloWorldExtension.java (87%) rename src/test/java/org/opensearch/sdk/sample/{ => helloworld}/rest/TestRestHelloAction.java (59%) diff --git a/DESIGN.md b/DESIGN.md index abf2462b..aa21d0fc 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -4,12 +4,12 @@ Plugin architecture enables extending core features of OpenSearch. There are various kinds of plugins which are supported. But, the architecture has significant problems for OpenSearch customers. Importantly, plugins can fatally impact the cluster -i.e critical workloads like ingestion/search traffic would be impacted because of a non-critical plugin like s3-repository failed with an exception. +i.e., critical workloads like ingestion/search traffic would be impacted because of a non-critical plugin like s3-repository failed with an exception. -This problem is exponentially grows when we would like to run a 3rd Party plugin from the community. +This problem exponentially grows when we would like to run a third Party plugin from the community. As OpenSearch and plugins run in the same process, it brings in security risk, dependency conflicts and reduces the velocity of releases. -Introducing extensions, a simple and easy way to extend features of OpenSearch. It would support all plugin features and enable them to run in a seperate process or on another node via OpenSearch SDK Java. +Introducing extensions, a simple and easy way to extend features of OpenSearch. It would support all plugin features and enable them to run in a seperate process or on another node via OpenSearch SDK for Java (other SDKs will be developed). Meta Issue: [Steps to make OpenSearch extensible](https://github.com/opensearch-project/OpenSearch/issues/2447) Sandboxing: [Step towards modular architecture in OpenSearch](https://github.com/opensearch-project/OpenSearch/issues/1422) @@ -84,7 +84,7 @@ The `org.opensearch.sdk.sample` package contains a sample `HelloWorldExtension` (2, 3, 4) Using the `ExtensionSettings` from the extension, the `ExtensionsRunner` binds to the configured host and port. -(5, 6, 7) Using the `List` from the extension, the `ExtensionsRunner` stores each handler (Rest Action)'s restPath (method+URI) in a map, identifying the action to execute when that combination is received by the extension. +(5, 6, 7) Using the `List` from the extension, the `ExtensionsRunner` stores each handler (Rest Action)'s restPath (method+URI) in the `ExtensionRestPathRegistry`, identifying the action to execute when that combination is received by the extension. This registry internally uses the same `PathTrie` implementation as OpenSearch's `RestController`. ##### OpenSearch Startup, Extension Initialization, and REST Action Registration @@ -94,7 +94,7 @@ The `ExtensionsOrchestrator` reads a list of extensions present in `extensions.y (11, 12) The `ExtensionsOrchestrator` Initializes the extension using an `InitializeExtensionRequest`/`Response`, establishing the two-way transport mechanism. -(13) Each `Extension` retrieves all REST paths from its pathMap (the key set). +(13) Each `Extension` retrieves all of its REST paths from its `ExtensionRestPathRegistry`. (14, 15, 16) Each `Extension` sends a `RegisterRestActionsRequest` to the `RestActionsRequestHandler`, which registers a `RestSendToExtensionAction` with the `RestController` to handle each REST path (`Route`). These routes rely on a globally unique identifier for the extension which users will use in REST requests, presently the Extension's `uniqueId`. @@ -110,9 +110,11 @@ The `ExtensionsOrchestrator` reads a list of extensions present in `extensions.y (21, 22) The appropriate `ExtensionRestHandler` handles the request, possibly executing complex logic, and eventually providing a response string. -(23, 24) The response string is relayed by the `Extension` to the `RestActionsRequestHandler` which uses it to complete the `RestSendToExtensionAction` by returning a `BytesRestResponse`. +(23, 24) As part of handling some requests, additional actions, such as creating an index, may require further interactions with OpenSearch's `RestController` which are accomplished via the `SDKClient` as required. -(25) The User receives the response. +(25, 26) The response string is relayed by the `Extension` to the `RestActionsRequestHandler` which uses it to complete the `RestSendToExtensionAction` by returning a `BytesRestResponse`. + +(27) The User receives the response. ## FAQ diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 42e5a8e4..bb616257 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -8,13 +8,13 @@ - [Run the Sample Extension](#run-the-sample-extension) - [Create extensions.yml file](#create-extensions-yml-file) - [Run OpenSearch](#run-opensearch) - - [Publish OpenSearch-SDK to Maven Local](#publish-opensearch-sdk-to-maven-local) + - [Publish OpenSearch SDK for Java to Maven Local](#publish-opensearch-sdk-for-java-to-maven-local) - [Perform a REST Request on the Extension](#perform-a-rest-request-on-the-extension) - [Run Tests](#run-tests) - [Submitting Changes](#submitting-changes) ## Introduction -Opensearch plugins have allowed the extension and ehancements of various core features however, current plugin architecture carries the risk of fatally impacting clusters should they fail. In order to ensure that plugins may run safely without impacting the system, our goal is to effectively isolate plugin interactions with OpenSearch by modularizing the [extension points](https://opensearch.org/blog/technical-post/2021/12/plugins-intro/) to which they hook onto. +OpenSearch plugins have allowed the extension and enhancements of various core features. However, the current plugin architecture carries the risk of fatally impacting clusters should they fail. In order to ensure that plugins may run safely without impacting the system, our goal is to effectively isolate plugin interactions with OpenSearch by modularizing the [extension points](https://opensearch.org/blog/technical-post/2021/12/plugins-intro/) to which they hook onto. Read more about extensibility [here](https://github.com/opensearch-project/OpenSearch/issues/1422) @@ -24,31 +24,32 @@ Read more about extensibility [here](https://github.com/opensearch-project/OpenS Fork [OpenSearch SDK for Java](https://github.com/opensearch-project/opensearch-sdk-java) and clone locally, e.g. `git clone https://github.com/[your username]/opensearch-sdk-java.git`. ### Git Clone OpenSearch Repo -Fork [OpenSearch](https://github.com/opensearch-project/OpenSearch/), checkout feature/extensions branch, and clone locally, e.g. `git clone https://github.com/[your username]/OpenSearch.git`. +Fork [OpenSearch](https://github.com/opensearch-project/OpenSearch/), clone locally, e.g., `git clone https://github.com/[your username]/OpenSearch.git`, and checkout the `feature/extensions` branch. ## Publish OpenSearch feature/extensions Branch to Maven local -The work done to support the extensions framework is located on the `feature/extensions` branch of the OpenSearch project. It is necessary to publish the dependencies of this branch to your local maven repository prior to running OpenSearch SDK for Java on a seperate process. +The work done to support the extensions framework is located on the `feature/extensions` branch of the OpenSearch project. Until this branch is merged to `main`, it is necessary to publish the dependencies of this branch to your local maven repository prior to running an Extension on a separate process. - First navigate to the directory that OpenSearch has been cloned to - Checkout the correct branch, e.g. `git checkout feature/extensions`. -- Run `./gradlew publishToMavenLocal`. - Run `./gradlew check` to make sure the build is successful. - -It is necessary to publish dependencies to a local maven repository until this branch is merged to `main`, at which point all dependencies will be published to Maven central. +- Run `./gradlew publishToMavenLocal`. ## Run the Sample Extension -Navigate to the directory that OpenSearch-SDK-Java has been cloned to and run the Sample Extension's main method using `./gradlew run`. +Navigate to the directory that OpenSearch-SDK-Java has been cloned to. + +You can execute just the SDK's `ExtensionsRunner` main method with test settings using `./gradlew run`. ``` ./gradlew run ``` -This will execute the main script set within the root `build.gradle` file : +You can execute the sample Hello World extension using the `helloWorld` task: ``` -mainClassName = 'transportservice.ExtensionsRunner' +./gradlew helloWorld ``` + Bound addresses will then be logged to the terminal : ```bash @@ -56,8 +57,9 @@ Bound addresses will then be logged to the terminal : [main] INFO transportservice.TransportService - profile [test]: publish_address {127.0.0.1:5555}, bound_addresses {[::1]:5555}, {127.0.0.1:5555} ``` -## Publish OpenSearch-SDK to Maven local -Until we publish this repo to maven central. Publishing to maven local is the way to import the artifacts +## Publish OpenSearch SDK for Java to Maven local + +Until we publish this repo to maven central, publishing to maven local is the way for plugins (outside the sample packages) to import the artifacts: ``` ./gradlew publishToMavenLocal ``` @@ -111,6 +113,7 @@ During OpenSearch bootstrap, `ExtensionsOrchestrator` will then discover the ext OpenSearch SDK terminal will also log all requests and responses it receives from OpenSearch : TCP HandShake Request : + ``` 21:30:18.943 [opensearch[extension][transport_worker][T#7]] TRACE org.opensearch.latencytester.transportservice.netty4.OpenSearchLoggingHandler - [id: 0x37b22600, L:/127.0.0.1:4532 - R:/127.0.0.1:47766] READ: 55B +-------------------------------------------------+ @@ -125,6 +128,7 @@ MESSAGE RECEIVED:E«󀀀internal:tcp/handshake£·A ``` Extension Name Request / Response : + ``` 21:30:18.992 [opensearch[extension][transport_worker][T#6]] TRACE org.opensearch.latencytester.transportservice.netty4.OpenSearchLoggingHandler - [id: 0xb2be651b, L:/127.0.0.1:4532 - R:/127.0.0.1:47782] READ: 204B +-------------------------------------------------+ diff --git a/Docs/ExtensionRestActions.svg b/Docs/ExtensionRestActions.svg index 9b568602..c1cddfb7 100644 --- a/Docs/ExtensionRestActions.svg +++ b/Docs/ExtensionRestActions.svg @@ -1 +1 @@ -title%20Extension%20Initialization%20and%20REST%20Request%2FResponse%20Sequence%0A%0Aentryspacing%200.3%0Aparticipantgroup%20%23lightgreen%20**Extension%20Node**%0Aentity%20Extension%0Aparticipant%20%22Extensions%5CnRunner%22%20as%20ExtensionsRunner%0Aend%0A%0Aparticipantgroup%20%23lightblue%20**OpenSearch%20Node**%0Aentity%20Node%0Aparticipant%20%22Extensions%5CnOrchestrator%22%20as%20ExtensionsOrchestrator%0Aparticipant%20%22RestActions%5CnRequestHandler%22%20as%20RestActionsRequestHandler%0Aactor%20User%0Aparticipant%20%22Rest%5CnController%22%20as%20RestController%0Aend%0A%0Aparallel%0Anote%20over%20Extension%2CExtensionsRunner%3AExtension%20Startup%0Anote%20over%20Node%2CRestController%3AOpenSearch%20Node%20Startup%0Aparallel%20off%0A%0Aparallel%0Aautonumber%201%0AExtension-%3E*ExtensionsRunner%3AExtensions%5CnRunner.run(this)%0Aautonumber%208%0ANode-%3E*RestController%3Anew%0Aparallel%20off%0A%0Aautonumber%202%0AExtension%3C-ExtensionsRunner%3AgetExtension%5Cn%20%20%20%20%20Settings()%0AExtension--%3EExtensionsRunner%3Areturns%20settings%0A%0Aspace%20-2.2%0Aparallel%0AExtensionsRunner-%3EExtensionsRunner%3A%20bind%20to%20host%3Aport%0Aautonumber%209%0ANode-%3E*ExtensionsOrchestrator%3Anew%0Aparallel%20off%0A%0Aspace%20-2.2%0Adestroysilent%20Node%0Aspace%201.4%0Aautonumber%205%0AExtension%3C-ExtensionsRunner%3AgetExtension%5Cn%20%20%20%20%20RestHandlers()%0AExtension--%3EExtensionsRunner%3Areturns%20handlers%0Aspace%20-3.2%0Aparallel%0AExtensionsRunner-%3EExtensionsRunner%3Aput%20all%20handlers%5Cnin%20pathMap%0Aautonumber%2010%0AExtensionsOrchestrator-%3E*RestActionsRequestHandler%3Anew%0Aparallel%20off%0A%0Aspace%201%0Anote%20over%20ExtensionsRunner%2CRestController%3AInitialization%20and%20Registration%0A%0Agroup%20for%20each%20extension%0AExtensionsRunner%3C-ExtensionsOrchestrator%3AInitializeExtensionRequest%0AExtensionsRunner--%3EExtensionsOrchestrator%3AInitializeExtensionResponse%0Adestroysilent%20ExtensionsOrchestrator%0A%0AExtensionsRunner-%3EExtensionsRunner%3Aget%20all%20handlers%5Cnfrom%20pathMap%0Aspace%20-1.5%0AExtensionsRunner-%3ERestActionsRequestHandler%3ARegisterRestActionsRequest%0Aspace%20-1.5%0ARestActionsRequestHandler-%3ERestController%3A%5C%2F%5C%2F%20map%20all%20routes%20to%20extension%5Cnregister(SendToExtensionAction)%0A%0AExtensionsRunner%3C--RestActionsRequestHandler%3ARegisterRestActionsResponse%0Aspace%200%0Aend%0Aspace%201%0Anote%20over%20Extension%2CRestController%3ALater%2C%20a%20user%20sends%20a%20REST%20request%2C%20handled%20by%20RestController%0A%0Acreate%20User%0AUser-%3ERestController%3AREST%20request%0Adestroysilent%20User%0A%0ARestActionsRequestHandler%3C-RestController%3A%20%20%20%20%20invokes%20registered%5CnSendToExtensionAction%0AExtensionsRunner%3C-RestActionsRequestHandler%3ARestExecuteOnExtensionRequest%0AExtensionsRunner%3C-ExtensionsRunner%3Aget%20handler%5Cnfrom%20pathMap%0Aspace%20-3.2%0AExtension%3C-ExtensionsRunner%3A(on%20mapped%20action)%5Cn%20%20handleRequest()%0A%0AExtension--%3EExtensionsRunner%3Areturns%20response%0AExtensionsRunner--%3ERestActionsRequestHandler%3ARestExecuteOnExtensionResponse%0ARestActionsRequestHandler--%3ERestController%3A%20%20%20%20%20SendToExtensionAction%5Cnreturns%20BytesRestResponse%0ARestController-%3E*User%3Aresponse%0Adestroysilent%20User%0Aspace%20-3Extension Initialization and REST Request/Response SequenceExtension NodeOpenSearch NodeExtensionNodeExtension StartupOpenSearch Node StartupExtensionsRunnerExtensionsRunner.run(this)RestControllernewgetExtension     Settings()returns settingsbind to host:portExtensionsOrchestratornewgetExtension     RestHandlers()returns handlersput all handlersin pathMapRestActionsRequestHandlernew10 Initialization and RegistrationInitializeExtensionRequest11 InitializeExtensionResponse12 get all handlers13 from pathMapRegisterRestActionsRequest14 // map all routes to extension15 register(SendToExtensionAction)RegisterRestActionsResponse16 Later, a user sends a REST request, handled by RestControllerUserREST request17     invokes registered18 SendToExtensionActionRestExecuteOnExtensionRequest19 get handler20 from pathMap(on mapped action)21   handleRequest()returns response22 RestExecuteOnExtensionResponse23     SendToExtensionAction24 returns BytesRestResponseUserresponse25 for each extension \ No newline at end of file +title%20Extension%20Initialization%20and%20REST%20Request%2FResponse%20Sequence%0A%0Aentryspacing%200.3%0Aparticipantgroup%20%23lightgreen%20**Extension%20Node**%0Aentity%20Extension%0Aparticipant%20%22Extensions%5CnRunner%22%20as%20ExtensionsRunner%0Aend%0A%0Aparticipantgroup%20%23lightblue%20**OpenSearch%20Node**%0Aentity%20Node%0Aparticipant%20%22Extensions%5CnOrchestrator%22%20as%20ExtensionsOrchestrator%0Aparticipant%20%22RestActions%5CnRequestHandler%22%20as%20RestActionsRequestHandler%0Aactor%20User%0Aparticipant%20%22Rest%5CnController%22%20as%20RestController%0Aend%0A%0Aparallel%0Anote%20over%20Extension%2CExtensionsRunner%3AExtension%20Startup%0Anote%20over%20Node%2CRestController%3AOpenSearch%20Node%20Startup%0Aparallel%20off%0A%0Aparallel%0Aautonumber%201%0AExtension-%3E*ExtensionsRunner%3AExtensions%5CnRunner.run(this)%0Aautonumber%208%0ANode-%3E*RestController%3Anew%0Aparallel%20off%0A%0Aautonumber%202%0AExtension%3C-ExtensionsRunner%3AgetExtension%5Cn%20%20%20%20%20Settings()%0AExtension--%3EExtensionsRunner%3Areturns%20settings%0A%0Aspace%20-2.2%0Aparallel%0AExtensionsRunner-%3EExtensionsRunner%3A%20bind%20to%20host%3Aport%0Aautonumber%209%0ANode-%3E*ExtensionsOrchestrator%3Anew%0Aparallel%20off%0A%0Aspace%20-2.2%0Adestroysilent%20Node%0Aspace%201.4%0Aautonumber%205%0AExtension%3C-ExtensionsRunner%3AgetExtension%5Cn%20%20%20%20%20RestHandlers()%0AExtension--%3EExtensionsRunner%3Areturns%20handlers%0Aspace%20-3.2%0Aparallel%0AExtensionsRunner-%3EExtensionsRunner%3Aput%20all%20handlers%5Cnin%20Extension%5CnRestPathRegistry%0Aautonumber%2010%0AExtensionsOrchestrator-%3E*RestActionsRequestHandler%3Anew%0Aparallel%20off%0A%0Aspace%201%0Anote%20over%20ExtensionsRunner%2CRestController%3AInitialization%20and%20Registration%0A%0Agroup%20for%20each%20extension%0AExtensionsRunner%3C-ExtensionsOrchestrator%3AInitializeExtensionRequest%0AExtensionsRunner--%3EExtensionsOrchestrator%3AInitializeExtensionResponse%0Adestroysilent%20ExtensionsOrchestrator%0A%0AExtensionsRunner-%3EExtensionsRunner%3Aget%20all%20handlers%5Cnfrom%20Extension%5CnRestPathRegistry%0Aspace%20-1.5%0AExtensionsRunner-%3ERestActionsRequestHandler%3ARegisterRestActionsRequest%0Aspace%20-1.5%0ARestActionsRequestHandler-%3ERestController%3A%5C%2F%5C%2F%20map%20all%20routes%20to%20extension%5Cnregister(SendToExtensionAction)%0A%0AExtensionsRunner%3C--RestActionsRequestHandler%3ARegisterRestActionsResponse%0Aspace%200%0Aend%0Aspace%201%0Anote%20over%20Extension%2CRestController%3ALater%2C%20a%20user%20sends%20a%20REST%20request%2C%20handled%20by%20RestController%0A%0Acreate%20User%0AUser-%3ERestController%3AREST%20request%0Adestroysilent%20User%0A%0ARestActionsRequestHandler%3C-RestController%3A%20%20%20%20%20invokes%20registered%5CnSendToExtensionAction%0AExtensionsRunner%3C-RestActionsRequestHandler%3ARestExecuteOnExtensionRequest%0AExtensionsRunner%3C-ExtensionsRunner%3Aget%20handler%5Cnfrom%20Extension%5CnRestPathRegistry%0Aspace%20-3.2%0AExtension%3C-ExtensionsRunner%3A(on%20mapped%20action)%5Cn%20%20handleRequest()%0A%0Aspace%200%0Agroup%20as%20needed%0AExtension-%3ERestController%3AUse%20SDKClient%20for%20REST%20Actions%20on%20OpenSearch%20cluster%0AExtension%3C--RestController%3Aresponse%20to%20extension%0Aspace%200%0Aend%0A%0A%0AExtension--%3EExtensionsRunner%3Areturns%20response%0AExtensionsRunner--%3ERestActionsRequestHandler%3ARestExecuteOnExtensionResponse%0ARestActionsRequestHandler--%3ERestController%3A%20%20%20%20%20SendToExtensionAction%5Cnreturns%20BytesRestResponse%0ARestController-%3E*User%3Aresponse%0Adestroysilent%20User%0Aspace%20-3%0AExtension Initialization and REST Request/Response SequenceExtension NodeOpenSearch NodeExtensionNodeExtension StartupOpenSearch Node StartupExtensionsRunnerExtensionsRunner.run(this)RestControllernewgetExtension     Settings()returns settingsbind to host:portExtensionsOrchestratornewgetExtension     RestHandlers()returns handlersput all handlersin ExtensionRestPathRegistryRestActionsRequestHandlernew10 Initialization and RegistrationInitializeExtensionRequest11 InitializeExtensionResponse12 get all handlers13 from ExtensionRestPathRegistryRegisterRestActionsRequest14 // map all routes to extension15 register(SendToExtensionAction)RegisterRestActionsResponse16 Later, a user sends a REST request, handled by RestControllerUserREST request17     invokes registered18 SendToExtensionActionRestExecuteOnExtensionRequest19 get handler20 from ExtensionRestPathRegistry(on mapped action)21   handleRequest()Use SDKClient for REST Actions on OpenSearch cluster22 response to extension23 returns response24 RestExecuteOnExtensionResponse25     SendToExtensionAction26 returns BytesRestResponseUserresponse27 for each extensionas needed \ No newline at end of file diff --git a/build.gradle b/build.gradle index 767863bc..75a67fec 100644 --- a/build.gradle +++ b/build.gradle @@ -27,8 +27,8 @@ apply plugin: 'application' apply from: 'gradle/formatting.gradle' apply plugin: 'maven-publish' -mainClassName = 'org.opensearch.sdk.sample.HelloWorldExtension' - +mainClassName = 'org.opensearch.sdk.ExtensionsRunner' + group 'org.opensearch.sdk' version '1.0.0-SNAPSHOT' @@ -98,13 +98,17 @@ task javadocStrict(type: Javadoc) { classpath = sourceSets.main.runtimeClasspath options.addStringOption('Xdoclint:all', '-quiet') options.memberLevel = JavadocMemberLevel.PRIVATE - - // the netty4 package will eventually be published to mavenCentral - // See https://github.com/opensearch-project/OpenSearch/issues/3118 - exclude 'org/opensearch/sdk/netty4' } check.dependsOn javadocStrict +// this task runs the helloworld sample extension +task helloWorld(type: JavaExec) { + group = 'Execution' + description = 'Run HelloWorld Extension.' + mainClass = 'org.opensearch.sdk.sample.helloworld.HelloWorldExtension' + classpath = sourceSets.main.runtimeClasspath +} + test { useJUnitPlatform() jvmArgs '--enable-preview' diff --git a/src/main/java/org/opensearch/sdk/ExtensionRestHandler.java b/src/main/java/org/opensearch/sdk/ExtensionRestHandler.java index 9b2035a9..e02203eb 100644 --- a/src/main/java/org/opensearch/sdk/ExtensionRestHandler.java +++ b/src/main/java/org/opensearch/sdk/ExtensionRestHandler.java @@ -14,7 +14,6 @@ import org.opensearch.rest.RestHandler.Route; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.rest.RestResponse; /** * This interface defines methods which an extension REST handler (action) must provide. @@ -31,10 +30,11 @@ public interface ExtensionRestHandler { * Handles REST Requests forwarded from OpenSearch for a configured route on an extension. * Parameters are components of the {@link RestRequest} received from a user. * This method corresponds to the {@link BaseRestHandler#prepareRequest} method. + * As in that method, consumed parameters must be tracked and returned in the response. * * @param method A REST method. * @param uri The URI to handle. - * @return A {@link RestResponse} to the request. + * @return An {@link ExtensionRestResponse} to the request. */ - RestResponse handleRequest(Method method, String uri); + ExtensionRestResponse handleRequest(Method method, String uri); } diff --git a/src/main/java/org/opensearch/sdk/ExtensionRestPathRegistry.java b/src/main/java/org/opensearch/sdk/ExtensionRestPathRegistry.java new file mode 100644 index 00000000..26d0c556 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ExtensionRestPathRegistry.java @@ -0,0 +1,70 @@ +/* + * 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.sdk; + +import java.util.ArrayList; +import java.util.List; + +import org.opensearch.common.path.PathTrie; +import org.opensearch.rest.RestUtils; +import org.opensearch.rest.RestRequest.Method; + +/** + * This class registers REST paths from extension Rest Handlers. + */ +public class ExtensionRestPathRegistry { + + // PathTrie to match paths to handlers + private PathTrie pathTrie = new PathTrie<>(RestUtils.REST_DECODER); + // List to return registered handlers + private List registeredPaths = new ArrayList<>(); + + /** + * Register a REST handler to handle a method and route in this extension's path registry. + * + * @param method The method to register. + * @param uri The URI to register. May include named wildcards. + * @param extensionRestHandler The RestHandler to handle this route + */ + public void registerHandler(Method method, String uri, ExtensionRestHandler extensionRestHandler) { + String restPath = restPathToString(method, uri); + pathTrie.insert(restPath, extensionRestHandler); + registeredPaths.add(restPath); + } + + /** + * Get the registered REST handler for the specified method and URI. + * + * @param method the registered method. + * @param uri the registered URI. + * @return The REST handler registered to handle this method and URI combination if found, null otherwise. + */ + public ExtensionRestHandler getHandler(Method method, String uri) { + return pathTrie.retrieve(restPathToString(method, uri)); + } + + /** + * List the registered routes. + * + * @return A list of strings identifying the registered routes. + */ + public List getRegisteredPaths() { + return registeredPaths; + } + + /** + * Converts a REST method and URI to a string. + * + * @param method the method. + * @param uri the URI. + * @return A string appending the method and URI. + */ + public static String restPathToString(Method method, String uri) { + return method.name() + " " + uri; + } +} diff --git a/src/main/java/org/opensearch/sdk/ExtensionRestResponse.java b/src/main/java/org/opensearch/sdk/ExtensionRestResponse.java new file mode 100644 index 00000000..c623c3ec --- /dev/null +++ b/src/main/java/org/opensearch/sdk/ExtensionRestResponse.java @@ -0,0 +1,93 @@ +/* + * 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.sdk; + +import java.util.List; + +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.xcontent.XContentBuilder; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestStatus; + +/** + * A subclass of {@link BytesRestResponse} which processes the consumed parameters into a custom header. + */ +public class ExtensionRestResponse extends BytesRestResponse { + + /** + * Key passed in {@link BytesRestResponse} headers to identify parameters consumed by the handler. For internal use. + */ + static final String CONSUMED_PARAMS_KEY = "extension.consumed.parameters"; + + /** + * Creates a new response based on {@link XContentBuilder}. + * + * @param status The REST status. + * @param builder The builder for the response. + * @param consumedParams Parameters consumed by the handler. + */ + public ExtensionRestResponse(RestStatus status, XContentBuilder builder, List consumedParams) { + super(status, builder); + addConsumedParamHeader(consumedParams); + } + + /** + * Creates a new plain text response. + * + * @param status The REST status. + * @param content A plain text response string. + * @param consumedParams Parameters consumed by the handler. + */ + public ExtensionRestResponse(RestStatus status, String content, List consumedParams) { + super(status, content); + addConsumedParamHeader(consumedParams); + } + + /** + * Creates a new plain text response. + * + * @param status The REST status. + * @param contentType The content type of the response string. + * @param content A response string. + * @param consumedParams Parameters consumed by the handler. + */ + public ExtensionRestResponse(RestStatus status, String contentType, String content, List consumedParams) { + super(status, contentType, content); + addConsumedParamHeader(consumedParams); + } + + /** + * Creates a binary response. + * + * @param status The REST status. + * @param contentType The content type of the response bytes. + * @param content Response bytes. + * @param consumedParams Parameters consumed by the handler. + */ + public ExtensionRestResponse(RestStatus status, String contentType, byte[] content, List consumedParams) { + super(status, contentType, content); + addConsumedParamHeader(consumedParams); + } + + /** + * Creates a binary response. + * + * @param status The REST status. + * @param contentType The content type of the response bytes. + * @param content Response bytes. + * @param consumedParams Parameters consumed by the handler. + */ + public ExtensionRestResponse(RestStatus status, String contentType, BytesReference content, List consumedParams) { + super(status, contentType, content); + addConsumedParamHeader(consumedParams); + } + + private void addConsumedParamHeader(List consumedParams) { + consumedParams.stream().forEach(p -> addHeader(CONSUMED_PARAMS_KEY, p)); + } +} diff --git a/src/main/java/org/opensearch/sdk/ExtensionsRunner.java b/src/main/java/org/opensearch/sdk/ExtensionsRunner.java index aaa563ca..722621aa 100644 --- a/src/main/java/org/opensearch/sdk/ExtensionsRunner.java +++ b/src/main/java/org/opensearch/sdk/ExtensionsRunner.java @@ -56,11 +56,9 @@ import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -78,10 +76,10 @@ public class ExtensionsRunner { private static final Logger logger = LogManager.getLogger(ExtensionsRunner.class); private static final String NODE_NAME_SETTING = "node.name"; - private Map extensionRestPathMap = new HashMap<>(); private String uniqueId; private DiscoveryNode opensearchNode; private TransportService extensionTransportService = null; + private ExtensionRestPathRegistry extensionRestPathRegistry = new ExtensionRestPathRegistry(); private final Settings settings; private final TransportInterceptor NOOP_TRANSPORT_INTERCEPTOR = new TransportInterceptor() { @@ -123,8 +121,7 @@ private ExtensionsRunner(Extension extension) throws IOException { // store rest handlers in the map for (ExtensionRestHandler extensionRestHandler : extension.getExtensionRestHandlers()) { for (Route route : extensionRestHandler.routes()) { - String restPath = route.getMethod().name() + " " + route.getPath(); - extensionRestPathMap.put(restPath, extensionRestHandler); + extensionRestPathRegistry.registerHandler(route.getMethod(), route.getPath(), extensionRestHandler); } } // initialize the transport service @@ -233,10 +230,12 @@ ExtensionBooleanResponse handleIndicesModuleNameRequest(IndicesModuleRequest ind */ RestExecuteOnExtensionResponse handleRestExecuteOnExtensionRequest(RestExecuteOnExtensionRequest request) { - String restPath = request.getMethod().name() + " " + request.getUri(); - ExtensionRestHandler restHandler = extensionRestPathMap.get(restPath); + ExtensionRestHandler restHandler = extensionRestPathRegistry.getHandler(request.getMethod(), request.getUri()); if (restHandler == null) { - return new RestExecuteOnExtensionResponse(RestStatus.INTERNAL_SERVER_ERROR, "No handler for " + restPath); + return new RestExecuteOnExtensionResponse( + RestStatus.NOT_FOUND, + "No handler for " + ExtensionRestPathRegistry.restPathToString(request.getMethod(), request.getUri()) + ); } // Get response from extension RestResponse response = restHandler.handleRequest(request.getMethod(), request.getUri()); @@ -401,7 +400,7 @@ public void startTransportService(TransportService transportService) { * @param transportService The TransportService defining the connection to OpenSearch. */ public void sendRegisterRestActionsRequest(TransportService transportService) { - List extensionRestPaths = new ArrayList<>(extensionRestPathMap.keySet()); + List extensionRestPaths = extensionRestPathRegistry.getRegisteredPaths(); logger.info("Sending Register REST Actions request to OpenSearch for " + extensionRestPaths); RegisterRestActionsResponseHandler registerActionsResponseHandler = new RegisterRestActionsResponseHandler(); try { diff --git a/src/main/java/org/opensearch/sdk/sample/HelloWorldExtension.java b/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java similarity index 95% rename from src/main/java/org/opensearch/sdk/sample/HelloWorldExtension.java rename to src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java index 5d74c0f1..a3c24b71 100644 --- a/src/main/java/org/opensearch/sdk/sample/HelloWorldExtension.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java @@ -5,7 +5,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.sdk.sample; +package org.opensearch.sdk.sample.helloworld; import java.io.IOException; import java.util.List; @@ -14,7 +14,7 @@ import org.opensearch.sdk.ExtensionRestHandler; import org.opensearch.sdk.ExtensionSettings; import org.opensearch.sdk.ExtensionsRunner; -import org.opensearch.sdk.sample.rest.RestHelloAction; +import org.opensearch.sdk.sample.helloworld.rest.RestHelloAction; /** * Sample class to demonstrate how to use the OpenSearch SDK for Java to create @@ -30,7 +30,7 @@ public class HelloWorldExtension implements Extension { /** * Optional classpath-relative path to a yml file containing extension settings. */ - private static final String EXTENSION_SETTINGS_PATH = "/sample/extension-settings.yml"; + private static final String EXTENSION_SETTINGS_PATH = "/sample/helloworld-settings.yml"; /** * The extension settings include a name, host address, and port. diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java new file mode 100644 index 00000000..cc10ee23 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java @@ -0,0 +1,65 @@ +/* + * 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.sdk.sample.helloworld.rest; + +import org.opensearch.rest.RestHandler.Route; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.sdk.ExtensionRestHandler; +import org.opensearch.sdk.ExtensionRestResponse; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.PUT; +import static org.opensearch.rest.RestStatus.BAD_REQUEST; +import static org.opensearch.rest.RestStatus.NOT_FOUND; +import static org.opensearch.rest.RestStatus.OK; + +/** + * Sample REST Handler (REST Action). Extension REST handlers must implement {@link ExtensionRestHandler}. + */ +public class RestHelloAction implements ExtensionRestHandler { + + private static final String GREETING = "Hello, %s!"; + private String worldName = "World"; + + @Override + public List routes() { + return List.of(new Route(GET, "/hello"), new Route(PUT, "/hello/{name}")); + } + + @Override + public ExtensionRestResponse handleRequest(Method method, String uri) { + // We need to track which parameters are consumed to pass back to OpenSearch + List consumedParams = new ArrayList<>(); + if (Method.GET.equals(method) && "/hello".equals(uri)) { + return new ExtensionRestResponse(OK, String.format(GREETING, worldName), consumedParams); + } else if (Method.PUT.equals(method) && uri.startsWith("/hello/")) { + // Placeholder code here for parameters in named wildcard paths + // Full implementation based on params() will be implemented as part of + // https://github.com/opensearch-project/opensearch-sdk-java/issues/111 + String name = uri.substring("/hello/".length()); + consumedParams.add("name"); + try { + worldName = URLDecoder.decode(name, StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + return new ExtensionRestResponse(BAD_REQUEST, e.getMessage(), consumedParams); + } + return new ExtensionRestResponse(OK, "Updated the world's name to " + worldName, consumedParams); + } + return new ExtensionRestResponse( + NOT_FOUND, + "Extension REST action improperly configured to handle " + method.name() + " " + uri, + consumedParams + ); + } + +} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json b/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json new file mode 100644 index 00000000..03cb9c58 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json @@ -0,0 +1,89 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Hello World", + "description": "This is a sample Hello World extension.", + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.0-SNAPSHOT" + }, + "tags": [ + { + "name": "hello", + "description": "Worldly Greetings" + } + ], + "paths": { + "/hello": { + "get": { + "tags": [ + "hello" + ], + "summary": "Greet the world", + "description": "Traditional greeting", + "responses": { + "200": { + "description": "Successful operation", + "content": { + "text/plain; charset=utf-8": { + "examples": { + "Default Response": { + "value": "Hello, World!" + } + } + } + } + }, + "400": { + "description": "Syntax Error in URI" + }, + "404": { + "description": "Improper REST action configuration" + } + } + } + }, + "/hello/{name}": { + "put": { + "tags": [ + "hello" + ], + "summary": "Update world name", + "description": "Rename the world", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "a new name for the world", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "text/plain; charset=utf-8": { + "examples": { + "Default Response": { + "value": "Updated the world's name to OpenSearch" + } + } + } + } + }, + "400": { + "description": "Syntax Error in URI" + }, + "404": { + "description": "Improper REST action configuration" + } + } + } + } + } +} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml b/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml new file mode 100644 index 00000000..4a35fe38 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml @@ -0,0 +1,55 @@ +openapi: 3.0.3 +info: + title: Hello World + description: This is a sample Hello World extension. + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.0-SNAPSHOT +tags: + - name: hello + description: Worldly Greetings +paths: + /hello: + get: + tags: + - hello + summary: Greet the world + description: Traditional greeting + responses: + '200': + description: Successful operation + content: + text/plain; charset=utf-8: + examples: + Default Response: + value: Hello, World! + '400': + description: Syntax Error in URI + '404': + description: Improper REST action configuration + /hello/{name}: + put: + tags: + - hello + summary: Update world name + description: Rename the world + parameters: + - name: name + in: path + description: a new name for the world + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + text/plain; charset=utf-8: + examples: + Default Response: + value: Updated the world's name to OpenSearch + '400': + description: Syntax Error in URI + '404': + description: Improper REST action configuration diff --git a/src/main/java/org/opensearch/sdk/sample/rest/RestHelloAction.java b/src/main/java/org/opensearch/sdk/sample/rest/RestHelloAction.java deleted file mode 100644 index 667ca7e1..00000000 --- a/src/main/java/org/opensearch/sdk/sample/rest/RestHelloAction.java +++ /dev/null @@ -1,46 +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.sdk.sample.rest; - -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestHandler.Route; -import org.opensearch.rest.RestRequest.Method; -import org.opensearch.rest.RestResponse; -import org.opensearch.sdk.ExtensionRestHandler; - -import java.util.List; - -import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.GET; -import static org.opensearch.rest.RestStatus.OK; -import static org.opensearch.rest.RestStatus.INTERNAL_SERVER_ERROR; - -/** - * Sample REST Handler (REST Action). Extension REST handlers must implement {@link ExtensionRestHandler}. - */ -public class RestHelloAction implements ExtensionRestHandler { - - private static final String GREETING = "Hello, World!"; - - @Override - public List routes() { - return singletonList(new Route(GET, "/hello")); - } - - @Override - public RestResponse handleRequest(Method method, String uri) { - if (Method.GET.equals(method) && "/hello".equals(uri)) { - return new BytesRestResponse(OK, GREETING); - } - return new BytesRestResponse( - INTERNAL_SERVER_ERROR, - "Extension REST action improperly configured to handle " + method.name() + " " + uri - ); - } - -} diff --git a/src/main/resources/sample/extension-settings.yml b/src/main/resources/sample/helloworld-settings.yml similarity index 100% rename from src/main/resources/sample/extension-settings.yml rename to src/main/resources/sample/helloworld-settings.yml diff --git a/src/test/java/org/opensearch/sdk/TestExtensionRestPathRegistry.java b/src/test/java/org/opensearch/sdk/TestExtensionRestPathRegistry.java new file mode 100644 index 00000000..32ce8fa1 --- /dev/null +++ b/src/test/java/org/opensearch/sdk/TestExtensionRestPathRegistry.java @@ -0,0 +1,99 @@ +package org.opensearch.sdk; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opensearch.rest.RestHandler.Route; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.test.OpenSearchTestCase; + +public class TestExtensionRestPathRegistry extends OpenSearchTestCase { + + private ExtensionRestPathRegistry extensionRestPathRegistry = new ExtensionRestPathRegistry(); + + private ExtensionRestHandler fooHandler = new ExtensionRestHandler() { + @Override + public List routes() { + return List.of(new Route(Method.GET, "/foo")); + } + + @Override + public ExtensionRestResponse handleRequest(Method method, String uri) { + return null; + } + }; + private ExtensionRestHandler barHandler = new ExtensionRestHandler() { + @Override + public List routes() { + return List.of(new Route(Method.PUT, "/bar/{planet}")); + } + + @Override + public ExtensionRestResponse handleRequest(Method method, String uri) { + return null; + } + }; + private ExtensionRestHandler bazHandler = new ExtensionRestHandler() { + @Override + public List routes() { + return List.of(new Route(Method.POST, "/baz/{moon}/qux"), new Route(Method.PUT, "/bar/baz")); + } + + @Override + public ExtensionRestResponse handleRequest(Method method, String uri) { + return null; + } + }; + + @Override + @BeforeEach + public void setUp() throws Exception { + List handlerList = List.of(fooHandler, barHandler, bazHandler); + super.setUp(); + for (ExtensionRestHandler handler : handlerList) { + for (Route route : handler.routes()) { + extensionRestPathRegistry.registerHandler(route.getMethod(), route.getPath(), handler); + } + } + } + + @Test + public void testRegisterConflicts() { + // Can't register same exact name + assertThrows(IllegalArgumentException.class, () -> extensionRestPathRegistry.registerHandler(Method.GET, "/foo", fooHandler)); + // Can't register conflicting named wildcards + assertThrows( + IllegalArgumentException.class, + () -> extensionRestPathRegistry.registerHandler(Method.PUT, "/bar/{none}", barHandler) + ); + } + + @Test + public void testGetHandler() { + assertEquals(fooHandler, extensionRestPathRegistry.getHandler(Method.GET, "/foo")); + assertNull(extensionRestPathRegistry.getHandler(Method.PUT, "/foo")); + + // Exact match and wildcard match can overlap, exact takes priority + assertEquals(barHandler, extensionRestPathRegistry.getHandler(Method.PUT, "/bar/mars")); + assertEquals(bazHandler, extensionRestPathRegistry.getHandler(Method.PUT, "/bar/baz")); + assertNull(extensionRestPathRegistry.getHandler(Method.PUT, "/bar/mars/bar")); + + assertEquals(bazHandler, extensionRestPathRegistry.getHandler(Method.POST, "/baz/europa/qux")); + assertNull(extensionRestPathRegistry.getHandler(Method.POST, "/bar/europa")); + } + + @Test + public void testGetRegisteredPaths() { + List registeredPaths = extensionRestPathRegistry.getRegisteredPaths(); + assertTrue(registeredPaths.contains("GET /foo")); + assertTrue(registeredPaths.contains("PUT /bar/{planet}")); + assertTrue(registeredPaths.contains("PUT /bar/baz")); + assertTrue(registeredPaths.contains("POST /baz/{moon}/qux")); + } + + @Test + public void testRestPathToString() { + assertEquals("GET /foo", ExtensionRestPathRegistry.restPathToString(Method.GET, "/foo")); + } +} diff --git a/src/test/java/org/opensearch/sdk/TestExtensionRestResponse.java b/src/test/java/org/opensearch/sdk/TestExtensionRestResponse.java new file mode 100644 index 00000000..e11aed5e --- /dev/null +++ b/src/test/java/org/opensearch/sdk/TestExtensionRestResponse.java @@ -0,0 +1,111 @@ +package org.opensearch.sdk; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.xcontent.XContentBuilder; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.test.OpenSearchTestCase; + +import static org.opensearch.rest.BytesRestResponse.TEXT_CONTENT_TYPE; +import static org.opensearch.rest.RestStatus.ACCEPTED; +import static org.opensearch.rest.RestStatus.OK; +import static org.opensearch.sdk.ExtensionRestResponse.CONSUMED_PARAMS_KEY; + +public class TestExtensionRestResponse extends OpenSearchTestCase { + + private static final String OCTET_CONTENT_TYPE = "application/octet-stream"; + private static final String JSON_CONTENT_TYPE = "application/json; charset=UTF-8"; + + private String testText; + private byte[] testBytes; + private List testConsumedParams; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + testText = "plain text"; + testBytes = new byte[] { 1, 2 }; + testConsumedParams = List.of("foo", "bar"); + } + + @Test + public void testConstructorWithBuilder() throws IOException { + XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent()); + builder.startObject(); + builder.field("status", ACCEPTED); + builder.endObject(); + ExtensionRestResponse response = new ExtensionRestResponse(OK, builder, testConsumedParams); + + assertEquals(OK, response.status()); + assertEquals(JSON_CONTENT_TYPE, response.contentType()); + assertEquals("{\"status\":\"ACCEPTED\"}", response.content().utf8ToString()); + List consumedParams = response.getHeaders().get(CONSUMED_PARAMS_KEY); + for (String param : consumedParams) { + assertTrue(testConsumedParams.contains(param)); + } + } + + @Test + public void testConstructorWithPlainText() { + ExtensionRestResponse response = new ExtensionRestResponse(OK, testText, testConsumedParams); + + assertEquals(OK, response.status()); + assertEquals(TEXT_CONTENT_TYPE, response.contentType()); + assertEquals(testText, response.content().utf8ToString()); + List consumedParams = response.getHeaders().get(CONSUMED_PARAMS_KEY); + for (String param : consumedParams) { + assertTrue(testConsumedParams.contains(param)); + } + } + + @Test + public void testConstructorWithText() { + ExtensionRestResponse response = new ExtensionRestResponse(OK, TEXT_CONTENT_TYPE, testText, testConsumedParams); + + assertEquals(OK, response.status()); + assertEquals(TEXT_CONTENT_TYPE, response.contentType()); + assertEquals(testText, response.content().utf8ToString()); + + List consumedParams = response.getHeaders().get(CONSUMED_PARAMS_KEY); + for (String param : consumedParams) { + assertTrue(testConsumedParams.contains(param)); + } + } + + @Test + public void testConstructorWithByteArray() { + ExtensionRestResponse response = new ExtensionRestResponse(OK, OCTET_CONTENT_TYPE, testBytes, testConsumedParams); + + assertEquals(OK, response.status()); + assertEquals(OCTET_CONTENT_TYPE, response.contentType()); + assertArrayEquals(testBytes, BytesReference.toBytes(response.content())); + List consumedParams = response.getHeaders().get(CONSUMED_PARAMS_KEY); + for (String param : consumedParams) { + assertTrue(testConsumedParams.contains(param)); + } + } + + @Test + public void testConstructorWithBytesReference() { + ExtensionRestResponse response = new ExtensionRestResponse( + OK, + OCTET_CONTENT_TYPE, + BytesReference.fromByteBuffer(ByteBuffer.wrap(testBytes, 0, 2)), + testConsumedParams + ); + + assertEquals(OK, response.status()); + assertEquals(OCTET_CONTENT_TYPE, response.contentType()); + assertArrayEquals(testBytes, BytesReference.toBytes(response.content())); + List consumedParams = response.getHeaders().get(CONSUMED_PARAMS_KEY); + for (String param : consumedParams) { + assertTrue(testConsumedParams.contains(param)); + } + } +} diff --git a/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java b/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java index 30bd6c11..dc53eaf1 100644 --- a/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java +++ b/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java @@ -153,7 +153,7 @@ public void testHandleRestExecuteOnExtensionRequest() throws Exception { RestExecuteOnExtensionRequest request = new RestExecuteOnExtensionRequest(Method.GET, "/foo"); RestExecuteOnExtensionResponse response = extensionsRunner.handleRestExecuteOnExtensionRequest(request); // this will fail in test environment with no registered actions - assertEquals(RestStatus.INTERNAL_SERVER_ERROR, response.getStatus()); + assertEquals(RestStatus.NOT_FOUND, response.getStatus()); assertEquals(BytesRestResponse.TEXT_CONTENT_TYPE, response.getContentType()); String responseStr = new String(response.getContent(), StandardCharsets.UTF_8); assertTrue(responseStr.contains("GET")); diff --git a/src/test/java/org/opensearch/sdk/sample/TestHelloWorldExtension.java b/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java similarity index 87% rename from src/test/java/org/opensearch/sdk/sample/TestHelloWorldExtension.java rename to src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java index 5729d976..bac31b4d 100644 --- a/src/test/java/org/opensearch/sdk/sample/TestHelloWorldExtension.java +++ b/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java @@ -5,14 +5,13 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.sdk.sample; +package org.opensearch.sdk.sample.helloworld; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opensearch.rest.RestHandler.Route; -import org.opensearch.rest.RestRequest.Method; import org.opensearch.sdk.Extension; import org.opensearch.sdk.ExtensionRestHandler; import org.opensearch.sdk.ExtensionSettings; @@ -44,9 +43,7 @@ public void testExtensionRestHandlers() { List extensionRestHandlers = extension.getExtensionRestHandlers(); assertEquals(1, extensionRestHandlers.size()); List routes = extensionRestHandlers.get(0).routes(); - assertEquals(1, routes.size()); - assertEquals(Method.GET, routes.get(0).getMethod()); - assertEquals("/hello", routes.get(0).getPath()); + assertEquals(2, routes.size()); } } diff --git a/src/test/java/org/opensearch/sdk/sample/rest/TestRestHelloAction.java b/src/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java similarity index 59% rename from src/test/java/org/opensearch/sdk/sample/rest/TestRestHelloAction.java rename to src/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java index 5a888b32..8c2d2e54 100644 --- a/src/test/java/org/opensearch/sdk/sample/rest/TestRestHelloAction.java +++ b/src/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java @@ -5,7 +5,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.sdk.sample.rest; +package org.opensearch.sdk.sample.helloworld.rest; import java.nio.charset.StandardCharsets; import java.util.List; @@ -35,9 +35,11 @@ public void setUp() throws Exception { @Test public void testRoutes() { List routes = restHelloAction.routes(); - assertEquals(1, routes.size()); + assertEquals(2, routes.size()); assertEquals(Method.GET, routes.get(0).getMethod()); assertEquals("/hello", routes.get(0).getPath()); + assertEquals(Method.PUT, routes.get(1).getMethod()); + assertEquals("/hello/{name}", routes.get(1).getPath()); } @Test @@ -49,13 +51,31 @@ public void testHandleRequest() { assertEquals("Hello, World!", responseStr); response = restHelloAction.handleRequest(Method.PUT, "/hello"); - assertEquals(RestStatus.INTERNAL_SERVER_ERROR, response.status()); + assertEquals(RestStatus.NOT_FOUND, response.status()); assertEquals(BytesRestResponse.TEXT_CONTENT_TYPE, response.contentType()); responseStr = new String(BytesReference.toBytes(response.content()), StandardCharsets.UTF_8); assertTrue(responseStr.contains("PUT")); + response = restHelloAction.handleRequest(Method.PUT, "/hello/Passing+Test"); + assertEquals(RestStatus.OK, response.status()); + assertEquals(BytesRestResponse.TEXT_CONTENT_TYPE, response.contentType()); + responseStr = new String(BytesReference.toBytes(response.content()), StandardCharsets.UTF_8); + assertEquals("Updated the world's name to Passing Test", responseStr); + + response = restHelloAction.handleRequest(Method.GET, "/hello"); + assertEquals(RestStatus.OK, response.status()); + assertEquals(BytesRestResponse.TEXT_CONTENT_TYPE, response.contentType()); + responseStr = new String(BytesReference.toBytes(response.content()), StandardCharsets.UTF_8); + assertEquals("Hello, Passing Test!", responseStr); + + response = restHelloAction.handleRequest(Method.PUT, "/hello/Bad%Request"); + assertEquals(RestStatus.BAD_REQUEST, response.status()); + assertEquals(BytesRestResponse.TEXT_CONTENT_TYPE, response.contentType()); + responseStr = new String(BytesReference.toBytes(response.content()), StandardCharsets.UTF_8); + assertTrue(responseStr.contains("Illegal hex characters in escape (%) pattern")); + response = restHelloAction.handleRequest(Method.GET, "/goodbye"); - assertEquals(RestStatus.INTERNAL_SERVER_ERROR, response.status()); + assertEquals(RestStatus.NOT_FOUND, response.status()); assertEquals(BytesRestResponse.TEXT_CONTENT_TYPE, response.contentType()); responseStr = new String(BytesReference.toBytes(response.content()), StandardCharsets.UTF_8); assertTrue(responseStr.contains("/goodbye"));