From fe6101ab5e7c5c4c02b76793ac43446580a6393a Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:10:04 -0800 Subject: [PATCH] [Feature] New module `geospatial-client` for cross-plugin IP enrichment (#700) (#705) * Common module Signed-off-by: Andy Kwok * POC Signed-off-by: Andy Kwok * Consolidate interface Signed-off-by: Andy Kwok * Consolidate interface Signed-off-by: Andy Kwok * Remove common module Signed-off-by: Andy Kwok * Gradle cleanup Signed-off-by: Andy Kwok * Lombok Signed-off-by: Andy Kwok * Java doc - 1 Signed-off-by: Andy Kwok * Java doc Signed-off-by: Andy Kwok * API Signature Signed-off-by: Andy Kwok * Fetch default data source Signed-off-by: Andy Kwok * Logger Signed-off-by: Andy Kwok * Initial test cases Signed-off-by: Andy Kwok * Add test-cases Signed-off-by: Andy Kwok * Test-cases: Client Signed-off-by: Andy Kwok * Update tests Signed-off-by: Andy Kwok * Update code style Signed-off-by: Andy Kwok * Style fix Signed-off-by: Andy Kwok * Update deps Signed-off-by: Andy Kwok * Spotless Signed-off-by: Andy Kwok * Spotless Signed-off-by: Andy Kwok * Code refactor Signed-off-by: Andy Kwok * Serialisation attempt Signed-off-by: Andy Kwok * Remove custom serialisation support Signed-off-by: Andy Kwok * Address code comments Signed-off-by: Andy Kwok * Update test infra Signed-off-by: Andy Kwok * Remove unused test-cases Signed-off-by: Andy Kwok * Add lombok config Signed-off-by: Andy Kwok * Update test Signed-off-by: Andy Kwok * Update changelog Signed-off-by: Andy Kwok * Update gradle Signed-off-by: Andy Kwok * Update Gradle build Signed-off-by: Andy Kwok * Update artifact info Signed-off-by: Andy Kwok * Minimise diff Signed-off-by: Andy Kwok * Style fix Signed-off-by: Andy Kwok * Code comments Signed-off-by: Andy Kwok --------- Signed-off-by: Andy Kwok (cherry picked from commit eb8aba6fe653a072e1b68fe62ed4d01cc2592ba0) Co-authored-by: Andy Kwok --- CHANGELOG.md | 1 + build.gradle | 1 + client/LICENSE.txt | 204 ++++++++++++++++++ client/NOTICE.txt | 2 + client/build.gradle | 80 +++++++ client/lombok.config | 5 + .../geospatial/action/IpEnrichmentAction.java | 24 +++ .../action/IpEnrichmentActionClient.java | 44 ++++ .../action/IpEnrichmentRequest.java | 98 +++++++++ .../action/IpEnrichmentResponse.java | 83 +++++++ .../action/IpEnrichmentActionClientTests.java | 59 +++++ .../action/IpEnrichmentRequestTests.java | 71 ++++++ .../action/IpEnrichmentResponseTests.java | 26 +++ settings.gradle | 5 +- .../action/IpEnrichmentTransportAction.java | 62 ++++++ .../geospatial/plugin/GeospatialPlugin.java | 8 + .../IpEnrichmentTransportActionTests.java | 46 ++++ 17 files changed, 818 insertions(+), 1 deletion(-) create mode 100644 client/LICENSE.txt create mode 100644 client/NOTICE.txt create mode 100644 client/build.gradle create mode 100644 client/lombok.config create mode 100644 client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentAction.java create mode 100644 client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentActionClient.java create mode 100644 client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentRequest.java create mode 100644 client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentResponse.java create mode 100644 client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentActionClientTests.java create mode 100644 client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentRequestTests.java create mode 100644 client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentResponseTests.java create mode 100644 src/main/java/org/opensearch/geospatial/ip2geo/action/IpEnrichmentTransportAction.java create mode 100644 src/test/java/org/opensearch/geospatial/ip2geo/action/IpEnrichmentTransportActionTests.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef8e2e8..767c2b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ See the [CONTRIBUTING guide](./CONTRIBUTING.md#Changelog) for instructions on ho ## [Unreleased 2.x](https://github.com/opensearch-project/geospatial/compare/2.17...2.x) ### Features +- Introduce new Java artifact geospatial-client to facilitate cross plugin communication. ([#700](https://github.com/opensearch-project/geospatial/pull/700)) ### Enhancements ### Bug Fixes ### Infrastructure diff --git a/build.gradle b/build.gradle index 0f34d9e6..868b81e0 100644 --- a/build.gradle +++ b/build.gradle @@ -162,6 +162,7 @@ configurations { dependencies { implementation "org.opensearch.plugin:geo:${opensearch_version}" api project(":libs:h3") + api project(":geospatial-client") yamlRestTestRuntimeOnly "org.apache.logging.log4j:log4j-core:${versions.log4j}" testImplementation "org.hamcrest:hamcrest:${versions.hamcrest}" testImplementation 'org.json:json:20231013' diff --git a/client/LICENSE.txt b/client/LICENSE.txt new file mode 100644 index 00000000..3ab280eb --- /dev/null +++ b/client/LICENSE.txt @@ -0,0 +1,204 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. diff --git a/client/NOTICE.txt b/client/NOTICE.txt new file mode 100644 index 00000000..76223dfd --- /dev/null +++ b/client/NOTICE.txt @@ -0,0 +1,2 @@ +OpenSearch (https://opensearch.org) +Copyright OpenSearch Contributors \ No newline at end of file diff --git a/client/build.gradle b/client/build.gradle new file mode 100644 index 00000000..582c6640 --- /dev/null +++ b/client/build.gradle @@ -0,0 +1,80 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'opensearch.build' +apply plugin: 'io.freefair.lombok' +apply plugin: "com.diffplug.spotless" + + +group = opensearch_group +version = "${opensearch_build}" +description = 'OpenSearch Geospatial client' + +project.loggerUsageCheck.enabled = false +project.testingConventions.enabled = false +project.forbiddenApisTest.enabled = false + +repositories { + mavenLocal() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } +} + +compileJava { + options.compilerArgs.addAll(["-processor", 'lombok.launch.AnnotationProcessorHider$AnnotationProcessor']) +} +compileTestJava { + options.compilerArgs.addAll(["-processor", 'lombok.launch.AnnotationProcessorHider$AnnotationProcessor']) +} + + +dependencies { + compileOnly "${group}:opensearch:${opensearch_version}" + + testImplementation "junit:junit:${versions.junit}" + testImplementation "org.mockito:mockito-core:${versions.mockito}" + testImplementation "org.hamcrest:hamcrest:${versions.hamcrest}" + testImplementation "net.bytebuddy:byte-buddy:${versions.bytebuddy}" + testImplementation "net.bytebuddy:byte-buddy-agent:${versions.bytebuddy}" +} + +licenseFile = "LICENSE.txt" +noticeFile = "NOTICE.txt" + +spotless { + java { + removeUnusedImports() + importOrder 'java', 'javax', 'org', 'com' + eclipse().configFile rootProject.file('formatterConfig.xml') + trimTrailingWhitespace() + endWithNewline() + } +} + +publishing { + publications { + pluginZip(MavenPublication) { publication -> + pom { + name = "opensearch-geospatial-client" + description = 'OpenSearch Geospatial client' + licenses { + license { + name = "The Apache License, Version 2.0" + url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + name = "OpenSearch" + url = "https://github.com/opensearch-project/geospatial" + } + } + } + } + } +} + + diff --git a/client/lombok.config b/client/lombok.config new file mode 100644 index 00000000..9745d1ed --- /dev/null +++ b/client/lombok.config @@ -0,0 +1,5 @@ +# tell lombok this is your root directory +config.stopBubbling = true +# add @lombok.Generated annotations to all generated nodes where possible +# to skip code coverage for auto generated code +lombok.addLombokGeneratedAnnotation = true diff --git a/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentAction.java b/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentAction.java new file mode 100644 index 00000000..963221bf --- /dev/null +++ b/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentAction.java @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.action; + +import org.opensearch.action.ActionType; +import org.opensearch.core.action.ActionResponse; + +/** + * An ActionType registered on OpenSearch registry, for inter-cluster transportAction call, + * to resolve GeoLocation for IP String. + */ +public class IpEnrichmentAction extends ActionType { + + public static final IpEnrichmentAction INSTANCE = new IpEnrichmentAction(); + + public static final String NAME = "cluster:admin/geospatial/ipenrichment/get"; + + public IpEnrichmentAction() { + super(NAME, IpEnrichmentResponse::new); + } +} diff --git a/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentActionClient.java b/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentActionClient.java new file mode 100644 index 00000000..2898e3fc --- /dev/null +++ b/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentActionClient.java @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.action; + +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.common.action.ActionFuture; +import org.opensearch.core.action.ActionResponse; + +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; + +/** + * Facade to provide GeoLocation enrichment for other plugins. + */ +@Log4j2 +@AllArgsConstructor +public class IpEnrichmentActionClient { + + final private NodeClient nodeClient; + + /** + * Client facing method, which read an IP in String form and return a map instance which contain the associated GeoLocation data. + * @param ipString IP v4 || v6 address in String form. + * @param datasourceName datasourceName in String form. + * @return A map instance which contain GeoLocation data for the given Ip address. + */ + public Map getGeoLocationData(String ipString, String datasourceName) throws ExecutionException, InterruptedException { + // Composite the request object. + ActionFuture responseActionFuture = nodeClient.execute( + IpEnrichmentAction.INSTANCE, + new IpEnrichmentRequest(ipString, datasourceName) + ); + // Send out the request and process the response. + ActionResponse genericActionResponse = responseActionFuture.get(); + IpEnrichmentResponse enrichmentResponse = IpEnrichmentResponse.fromActionResponse(genericActionResponse); + return enrichmentResponse.getGeoLocationData(); + } +} diff --git a/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentRequest.java b/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentRequest.java new file mode 100644 index 00000000..4ad92392 --- /dev/null +++ b/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentRequest.java @@ -0,0 +1,98 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.action; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.InputStreamStreamInput; +import org.opensearch.core.common.io.stream.OutputStreamStreamOutput; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.log4j.Log4j2; + +/** + * Wrapper for the IP 2 GeoLocation action request. + */ +@Getter +@Setter +@Log4j2 +@AllArgsConstructor +public class IpEnrichmentRequest extends ActionRequest { + + private String ipString; + + private String datasourceName; + + /** + * Constructor for TransportAction. + * @param streamInput the streamInput. + */ + public IpEnrichmentRequest(StreamInput streamInput) throws IOException { + super(streamInput); + ipString = streamInput.readString(); + datasourceName = streamInput.readString(); + log.trace("Constructing IP Enrichment request with values: [{}, {}]", ipString, datasourceName); + } + + /** + * Perform validation on the request, before GetSpatial processing it. + * @return Exception which contain validation errors, if any. + */ + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException errors = new ActionRequestValidationException(); + if (ipString == null) { + errors.addValidationError("ip string should not be null"); + } + if (datasourceName == null) { + errors.addValidationError("DateSource should not be null"); + } + return errors.validationErrors().isEmpty() ? null : errors; + } + + /** + * Overridden method to populate object's payload into StreamOutput form. + * @param out the StreamOutput object. + * @throws IOException If given StreamOutput is not compatible. + */ + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(ipString); + out.writeString(datasourceName); + } + + /** + * Static method get around the cast exception happen for cross plugin communication. + * @param actionRequest A casted-up version of IpEnrichmentRequest. + * @return IpEnrichmentRequest object which can be used within the scope of the caller. + */ + public static IpEnrichmentRequest fromActionRequest(ActionRequest actionRequest) { + // From the same classloader + if (actionRequest instanceof IpEnrichmentRequest) { + return (IpEnrichmentRequest) actionRequest; + } + + // Or else convert it + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); OutputStreamStreamOutput osso = new OutputStreamStreamOutput(baos)) { + actionRequest.writeTo(osso); + try (StreamInput input = new InputStreamStreamInput(new ByteArrayInputStream(baos.toByteArray()))) { + return new IpEnrichmentRequest(input); + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to parse ActionRequest into IpEnrichmentRequest", e); + } + } +} diff --git a/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentResponse.java b/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentResponse.java new file mode 100644 index 00000000..7f5c3007 --- /dev/null +++ b/client/src/main/java/org/opensearch/geospatial/action/IpEnrichmentResponse.java @@ -0,0 +1,83 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.action; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Map; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.InputStreamStreamInput; +import org.opensearch.core.common.io.stream.OutputStreamStreamOutput; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.log4j.Log4j2; + +/** + * Wrapper class to encapsulate the IP enrichment result for IpEnrichmentTransportAction. + */ +@Getter +@Setter +@Log4j2 +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class IpEnrichmentResponse extends ActionResponse { + + private Map geoLocationData; + + /** + * Public method to be called by fromActionResponse( ) to populate this Response class. + * @param streamInput Stream object which contain the geoLocationData. + * @throws IOException Exception being thrown when given stremInput doesn't contain what IpEnrichmentResponse is expecting. + */ + public IpEnrichmentResponse(StreamInput streamInput) throws IOException { + super(streamInput); + geoLocationData = streamInput.readMap(); + log.trace("Constructing IP Enrichment response with values: [{}]", geoLocationData); + } + + /** + * Overridden method used by OpenSearch runtime to serialise this class content into stream. + * @param streamOutput the streamOutput used to construct this response object. + * @throws IOException the IOException. + */ + @Override + public void writeTo(StreamOutput streamOutput) throws IOException { + streamOutput.writeMap(geoLocationData); + } + + /** + * Static method to convert a given ActionResponse to IpEnrichmentResponse by serialisation with streamOuput. + * This will be required for cross plugin communication scenario, as multiple class definition will be loaded + * by respective Plugin's classloader. + * @param actionResponse An IpEnrichmentResponse in casted-up form. + * @return An IpEnrichmentResponse object which contain the same payload as the incoming object. + */ + public static IpEnrichmentResponse fromActionResponse(ActionResponse actionResponse) { + // From the same classloader + if (actionResponse instanceof IpEnrichmentResponse) { + return (IpEnrichmentResponse) actionResponse; + } + + // Or else convert it + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); OutputStreamStreamOutput osso = new OutputStreamStreamOutput(baos)) { + actionResponse.writeTo(osso); + try (StreamInput input = new InputStreamStreamInput(new ByteArrayInputStream(baos.toByteArray()))) { + return new IpEnrichmentResponse(input); + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to parse ActionResponse into IpEnrichmentResponse", e); + } + } + +} diff --git a/client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentActionClientTests.java b/client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentActionClientTests.java new file mode 100644 index 00000000..e91f7aa3 --- /dev/null +++ b/client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentActionClientTests.java @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.action; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.opensearch.client.node.NodeClient; +import org.opensearch.common.action.ActionFuture; +import org.opensearch.core.action.ActionResponse; + +import lombok.SneakyThrows; + +@RunWith(MockitoJUnitRunner.class) +public class IpEnrichmentActionClientTests { + + @Mock + private NodeClient mockNodeClient; + + @Mock + private ActionFuture mockResult; + + String dummyIpString = "192.168.1.1"; + + String dummyDataSourceName = "testDataSource"; + + Map dummyPayload = Map.of("k1", "v1"); + + @SneakyThrows + @Test + public void testWithValidResponse() { + when(mockResult.get()).thenReturn(new IpEnrichmentResponse(dummyPayload)); + when(mockNodeClient.execute(eq(IpEnrichmentAction.INSTANCE), any())).thenReturn(mockResult); + IpEnrichmentActionClient ipClient = new IpEnrichmentActionClient(mockNodeClient); + Map actualPayload = ipClient.getGeoLocationData(dummyIpString, dummyDataSourceName); + Assert.assertEquals(dummyPayload, actualPayload); + } + + @SneakyThrows + @Test(expected = ExecutionException.class) + public void testWithException() { + when(mockResult.get()).thenThrow(new ExecutionException(new Throwable())); + when(mockNodeClient.execute(eq(IpEnrichmentAction.INSTANCE), any())).thenReturn(mockResult); + IpEnrichmentActionClient ipClient = new IpEnrichmentActionClient(mockNodeClient); + ipClient.getGeoLocationData(dummyIpString, dummyDataSourceName); + } +} diff --git a/client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentRequestTests.java b/client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentRequestTests.java new file mode 100644 index 00000000..af815ca4 --- /dev/null +++ b/client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentRequestTests.java @@ -0,0 +1,71 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.action; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test cases for IpEnrichmentRequest. + */ +public class IpEnrichmentRequestTests { + + /** + * Test validate() against a valid record. + */ + @Test + public void testValidateValidRequest() { + System.out.println("Test"); + IpEnrichmentRequest request = new IpEnrichmentRequest("192.168.1.1", "ValidDataSourceName"); + Assert.assertNull(request.validate()); + } + + /** + * Test validate() against an invalid record, + * Expecting an error being thrown as dataSource being null. + */ + @Test + public void testValidateNullDataSourceName() { + IpEnrichmentRequest request = new IpEnrichmentRequest("192.168.1.1", null); + Assert.assertEquals(1, request.validate().validationErrors().size()); + } + + /** + * Test validate() against an invalid record, + * Expecting an error being thrown as ipString being null. + */ + @Test + public void testValidateNullIpString() { + IpEnrichmentRequest request = new IpEnrichmentRequest(null, "dataSource"); + Assert.assertEquals(1, request.validate().validationErrors().size()); + } + + /** + * Test validate() against an invalid record, + * Expecting an error with size in 2, because both fields are null. + */ + @Test + public void testValidateNullIpStringAndDataSourceName() { + IpEnrichmentRequest request = new IpEnrichmentRequest(null, null); + Assert.assertEquals(2, request.validate().validationErrors().size()); + } + + /** + * Test fromActionRequest( ) to make sure the serialisation works. + */ + @Test + public void testFromActionRequestOnValidRecord() { + String ipString = "192.168.1.1"; + String dsName = "demo"; + IpEnrichmentRequest request = new IpEnrichmentRequest(ipString, dsName); + + IpEnrichmentRequest requestAfterStream = IpEnrichmentRequest.fromActionRequest(request); + + Assert.assertEquals(request.getIpString(), requestAfterStream.getIpString()); + Assert.assertEquals(request.getDatasourceName(), requestAfterStream.getDatasourceName()); + } + +} diff --git a/client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentResponseTests.java b/client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentResponseTests.java new file mode 100644 index 00000000..103b37af --- /dev/null +++ b/client/src/test/java/org/opensearch/geospatial/action/IpEnrichmentResponseTests.java @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.action; + +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +public class IpEnrichmentResponseTests { + + /** + * To simulate when Response class being passed from one plugin to the other. + */ + @Test + public void testFromActionResponseWithValidPayload() { + + Map payload = Map.of("k1", "v1"); + IpEnrichmentResponse response = new IpEnrichmentResponse(payload); + IpEnrichmentResponse castedResponse = IpEnrichmentResponse.fromActionResponse(response); + Assert.assertEquals(response.getGeoLocationData(), castedResponse.getGeoLocationData()); + } +} diff --git a/settings.gradle b/settings.gradle index 72251801..e4103866 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,4 +10,7 @@ rootProject.name = 'geospatial' include ":libs" -include ":libs:h3" \ No newline at end of file +include ":libs:h3" + +include 'client' +project(":client").name = rootProject.name + "-client" diff --git a/src/main/java/org/opensearch/geospatial/ip2geo/action/IpEnrichmentTransportAction.java b/src/main/java/org/opensearch/geospatial/ip2geo/action/IpEnrichmentTransportAction.java new file mode 100644 index 00000000..5932835c --- /dev/null +++ b/src/main/java/org/opensearch/geospatial/ip2geo/action/IpEnrichmentTransportAction.java @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.ip2geo.action; + +import java.util.Map; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.action.ActionResponse; +import org.opensearch.geospatial.action.IpEnrichmentAction; +import org.opensearch.geospatial.action.IpEnrichmentRequest; +import org.opensearch.geospatial.action.IpEnrichmentResponse; +import org.opensearch.geospatial.ip2geo.dao.Ip2GeoCachedDao; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import lombok.extern.log4j.Log4j2; + +/** + * Transport action to convert provided IP address String into GeoLocation data. + */ +@Log4j2 +public class IpEnrichmentTransportAction extends HandledTransportAction { + + private final Ip2GeoCachedDao ip2GeoCachedDao; + + /** + * Constructor + * @param transportService the transport service + * @param actionFilters the action filters + * @param cachedDao the cached datasource facade + */ + @Inject + public IpEnrichmentTransportAction(TransportService transportService, ActionFilters actionFilters, Ip2GeoCachedDao cachedDao) { + super(IpEnrichmentAction.NAME, transportService, actionFilters, IpEnrichmentRequest::new); + this.ip2GeoCachedDao = cachedDao; + } + + /** + * Overridden method to extract IP String from IpEnrichmentRequest object and return the enrichment result + * in the form of IpEnrichmentResponse which contains the GeoLocation data for given IP String. + * @param task the task. + * @param request request object in the form of IpEnrichmentRequest which contain the IP String to resolve + * @param listener a container which encapsulate IpEnrichmentResponse object with the GeoLocation data for given IP. + */ + @Override + protected void doExecute(Task task, ActionRequest request, ActionListener listener) { + IpEnrichmentRequest enrichmentRequest = IpEnrichmentRequest.fromActionRequest(request); + String ipString = enrichmentRequest.getIpString(); + String dataSourceName = enrichmentRequest.getDatasourceName(); + String indexName = ip2GeoCachedDao.getIndexName(dataSourceName); + Map geoLocationData = ip2GeoCachedDao.getGeoData(indexName, ipString); + log.debug("GeoSpatial IP lookup on IP: [{}], and result [{}]", ipString, geoLocationData); + listener.onResponse(new IpEnrichmentResponse(geoLocationData)); + } +} diff --git a/src/main/java/org/opensearch/geospatial/plugin/GeospatialPlugin.java b/src/main/java/org/opensearch/geospatial/plugin/GeospatialPlugin.java index d64f20b4..f0f45e1e 100644 --- a/src/main/java/org/opensearch/geospatial/plugin/GeospatialPlugin.java +++ b/src/main/java/org/opensearch/geospatial/plugin/GeospatialPlugin.java @@ -30,6 +30,7 @@ import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.env.Environment; import org.opensearch.env.NodeEnvironment; +import org.opensearch.geospatial.action.IpEnrichmentAction; import org.opensearch.geospatial.action.upload.geojson.UploadGeoJSONAction; import org.opensearch.geospatial.action.upload.geojson.UploadGeoJSONTransportAction; import org.opensearch.geospatial.index.mapper.xypoint.XYPointFieldMapper; @@ -41,6 +42,7 @@ import org.opensearch.geospatial.ip2geo.action.DeleteDatasourceTransportAction; import org.opensearch.geospatial.ip2geo.action.GetDatasourceAction; import org.opensearch.geospatial.ip2geo.action.GetDatasourceTransportAction; +import org.opensearch.geospatial.ip2geo.action.IpEnrichmentTransportAction; import org.opensearch.geospatial.ip2geo.action.PutDatasourceAction; import org.opensearch.geospatial.ip2geo.action.PutDatasourceTransportAction; import org.opensearch.geospatial.ip2geo.action.RestDeleteDatasourceHandler; @@ -221,9 +223,15 @@ public List getRestHandlers( new ActionHandler<>(DeleteDatasourceAction.INSTANCE, DeleteDatasourceTransportAction.class) ); + // Inter-cluster IP enrichment request + List> ipEnrichmentHandlers = List.of( + new ActionHandler<>(IpEnrichmentAction.INSTANCE, IpEnrichmentTransportAction.class) + ); + List> allHandlers = new ArrayList<>(); allHandlers.addAll(geoJsonHandlers); allHandlers.addAll(ip2geoHandlers); + allHandlers.addAll(ipEnrichmentHandlers); return allHandlers; } diff --git a/src/test/java/org/opensearch/geospatial/ip2geo/action/IpEnrichmentTransportActionTests.java b/src/test/java/org/opensearch/geospatial/ip2geo/action/IpEnrichmentTransportActionTests.java new file mode 100644 index 00000000..85745b11 --- /dev/null +++ b/src/test/java/org/opensearch/geospatial/ip2geo/action/IpEnrichmentTransportActionTests.java @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.ip2geo.action; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.mockito.Mock; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.action.ActionResponse; +import org.opensearch.geospatial.action.IpEnrichmentRequest; +import org.opensearch.geospatial.action.IpEnrichmentResponse; +import org.opensearch.geospatial.ip2geo.Ip2GeoTestCase; +import org.opensearch.tasks.Task; + +public class IpEnrichmentTransportActionTests extends Ip2GeoTestCase { + + private IpEnrichmentTransportAction action; + + @Mock + Task task; + + @Mock + ActionListener listener; + + @Before + public void init() { + action = new IpEnrichmentTransportAction(transportService, actionFilters, ip2GeoCachedDao); + } + + /** + * When dataSource is provided. + */ + public void testDoExecuteAllSucceed() { + IpEnrichmentRequest request = new IpEnrichmentRequest("192.168.1.1", "testSource"); + action.doExecute(task, request, listener); + + verify(listener, times(1)).onResponse(any(IpEnrichmentResponse.class)); + } + +}