KNOX-3222: Integrate OpenSearch REST API in Knox (#462) (#1131)

Co-authored-by: Tamas Payer <tpayer@cloudera.com>
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscovery.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscovery.java
index 32941d5..12d4cf7 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscovery.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscovery.java
@@ -39,6 +39,7 @@
 import org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitor;
 import org.apache.knox.gateway.topology.discovery.ServiceDiscovery;
 import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig;
+import org.apache.knox.gateway.topology.discovery.cm.model.opensearch.OpenSearchApiMasterServiceModelGenerator;
 import org.apache.knox.gateway.topology.discovery.cm.monitor.ClouderaManagerClusterConfigurationMonitor;
 
 import java.net.SocketException;
@@ -317,7 +318,7 @@
         ServiceRoleDetails serviceRoleDetails = new ServiceRoleDetails(service, serviceConfig, role, roleConfigs);
         log.discoveringServiceRole(role.getName(), role.getType());
 
-        Set<ServiceModel> modelsForRole = generateServiceModels(client, serviceRoleDetails, coreSettingsConfig, modelGenerators);
+        Set<ServiceModel> modelsForRole = generateServiceModels(client, serviceRoleDetails, coreSettingsConfig, modelGenerators, roleConfigList);
 
         log.discoveredServiceRole(role.getName(), role.getType());
 
@@ -331,11 +332,17 @@
     return serviceModels;
   }
 
-  private Set<ServiceModel> generateServiceModels(DiscoveryApiClient client, ServiceRoleDetails serviceRoleDetails, ApiServiceConfig coreSettingsConfig, List<ServiceModelGenerator> modelGenerators) throws ApiException {
+  private Set<ServiceModel> generateServiceModels(DiscoveryApiClient client, ServiceRoleDetails serviceRoleDetails,
+                                                  ApiServiceConfig coreSettingsConfig, List<ServiceModelGenerator> modelGenerators,
+                                                  ApiRoleConfigList roleConfigList) throws ApiException {
     Set<ServiceModel> serviceModels = new HashSet<>();
 
     if (modelGenerators != null) {
       for (ServiceModelGenerator serviceModelGenerator : modelGenerators) {
+        if (OpenSearchApiMasterServiceModelGenerator.shouldSkipGeneratorWhenOpenSearchMaster(serviceModelGenerator, roleConfigList)) {
+          continue;
+        }
+
         ServiceModel serviceModel = generateServiceModel(client, serviceRoleDetails, coreSettingsConfig, serviceModelGenerator);
         if (serviceModel != null) {
           serviceModels.add(serviceModel);
@@ -348,9 +355,9 @@
 
   private static ServiceModel generateServiceModel(DiscoveryApiClient client, ServiceRoleDetails sd,
                                                    ApiServiceConfig coreSettingsConfig, ServiceModelGenerator serviceModelGenerator) throws ApiException {
+    serviceModelGenerator.setApiClient(client);
     ServiceModelGeneratorHandleResponse response = serviceModelGenerator.handles(sd.getService(), sd.getServiceConfig(), sd.getRole(), sd.getRoleConfig());
     if (response.handled()) {
-      serviceModelGenerator.setApiClient(client);
       return serviceModelGenerator.generateService(sd.getService(), sd.getServiceConfig(), sd.getRole(), sd.getRoleConfig(), coreSettingsConfig);
     } else if (!response.getConfigurationIssues().isEmpty()) {
       log.serviceRoleHasConfigurationIssues(sd.getRole().getName(), String.join(";", response.getConfigurationIssues()));
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/AbstractOpenSearchApiServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/AbstractOpenSearchApiServiceModelGenerator.java
new file mode 100644
index 0000000..15a3862
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/AbstractOpenSearchApiServiceModelGenerator.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you 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.
+ */
+package org.apache.knox.gateway.topology.discovery.cm.model.opensearch;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModel;
+
+public abstract class AbstractOpenSearchApiServiceModelGenerator extends AbstractOpenSearchServiceModelGenerator {
+  @Override
+  public ServiceModel.Type getModelType() {
+    return ServiceModel.Type.API;
+  }
+
+  @Override
+  protected String getPortConfigName() {
+    return "opensearch_http_port";
+  }
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/AbstractOpenSearchServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/AbstractOpenSearchServiceModelGenerator.java
new file mode 100644
index 0000000..f573a57
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/AbstractOpenSearchServiceModelGenerator.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you 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.
+ */
+package org.apache.knox.gateway.topology.discovery.cm.model.opensearch;
+
+import com.cloudera.api.swagger.client.ApiException;
+import com.cloudera.api.swagger.model.ApiConfigList;
+import com.cloudera.api.swagger.model.ApiRole;
+import com.cloudera.api.swagger.model.ApiService;
+import com.cloudera.api.swagger.model.ApiServiceConfig;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModel;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGenerator;
+
+import java.util.Locale;
+
+public abstract class AbstractOpenSearchServiceModelGenerator extends AbstractServiceModelGenerator {
+  public static final String SERVICE = "OPENSEARCH";
+  public static final String SERVICE_TYPE = "OPENSEARCH";
+
+  @Override
+  public String getService() {
+    return SERVICE;
+  }
+
+  @Override
+  public String getServiceType() {
+    return SERVICE_TYPE;
+  }
+
+  protected String getSSLEnabledConfigName() {
+    return "ssl_enabled";
+  }
+
+  protected abstract String getPortConfigName();
+
+  @Override
+  public ServiceModel generateService(ApiService service, ApiServiceConfig serviceConfig, ApiRole role,
+                                      ApiConfigList roleConfig, ApiServiceConfig coreSettingsConfig) throws ApiException {
+    final String hostname = role.getHostRef().getHostname();
+    final String port = getRoleConfigValue(roleConfig, getPortConfigName());
+    final String sslEnabled = getRoleConfigValue(roleConfig, getSSLEnabledConfigName());
+
+    final String scheme = Boolean.parseBoolean(sslEnabled) ? "https" : "http";
+    final String url = String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port);
+
+    final ServiceModel model = createServiceModel(url);
+    model.addRoleProperty(getRoleType(), getPortConfigName(), port);
+    model.addRoleProperty(getRoleType(), getSSLEnabledConfigName(), sslEnabled);
+
+    return model;
+  }
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiCoordinatorServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiCoordinatorServiceModelGenerator.java
new file mode 100644
index 0000000..0dc5d58
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiCoordinatorServiceModelGenerator.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you 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.
+ */
+package org.apache.knox.gateway.topology.discovery.cm.model.opensearch;
+
+public class OpenSearchApiCoordinatorServiceModelGenerator extends AbstractOpenSearchApiServiceModelGenerator {
+  static final String OPENSEARCH_COORDINATOR_ROLE_TYPE = "OPENSEARCH_COORDINATOR";
+
+  @Override
+  public String getRoleType() {
+    return OPENSEARCH_COORDINATOR_ROLE_TYPE;
+  }
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiMasterServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiMasterServiceModelGenerator.java
new file mode 100644
index 0000000..90f9413
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiMasterServiceModelGenerator.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you 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.
+ */
+package org.apache.knox.gateway.topology.discovery.cm.model.opensearch;
+
+import com.cloudera.api.swagger.model.ApiRoleConfigList;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+
+public class OpenSearchApiMasterServiceModelGenerator extends AbstractOpenSearchApiServiceModelGenerator {
+  static final String OPENSEARCH_MASTER_ROLE_TYPE = "OPENSEARCH_MASTER";
+
+  @Override
+  public String getRoleType() {
+      return OPENSEARCH_MASTER_ROLE_TYPE;
+  }
+
+  /**
+   * If here is an OPENSEARCH_COORDINATOR role for this service o not generate service model for
+   * OPENSEARCH_MASTER role because OPENSEARCH_COORDINATOR is the preferred REST API target.
+   * @param serviceModelGenerator the actual model generator
+   * @param roles all the roles of the service
+   * @return <code>true</code> if <code>serviceModelGenerator</code> is for OpenSearch Master and there are Coordinator roles in the cluster.
+   */
+  public static boolean shouldSkipGeneratorWhenOpenSearchMaster(ServiceModelGenerator serviceModelGenerator, ApiRoleConfigList roles) {
+    if (!AbstractOpenSearchServiceModelGenerator.SERVICE_TYPE.equals(serviceModelGenerator.getServiceType())
+            || !OpenSearchApiMasterServiceModelGenerator.OPENSEARCH_MASTER_ROLE_TYPE.equals(serviceModelGenerator.getRoleType())) {
+      return false;
+    }
+    return roles.getItems().stream().anyMatch(r -> r.getRoleType().equals(OpenSearchApiCoordinatorServiceModelGenerator.OPENSEARCH_COORDINATOR_ROLE_TYPE));
+  }
+}
diff --git a/gateway-discovery-cm/src/main/resources/META-INF/services/org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator b/gateway-discovery-cm/src/main/resources/META-INF/services/org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator
index 1fef3b6..f068eb0 100644
--- a/gateway-discovery-cm/src/main/resources/META-INF/services/org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator
+++ b/gateway-discovery-cm/src/main/resources/META-INF/services/org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator
@@ -61,5 +61,5 @@
 org.apache.knox.gateway.topology.discovery.cm.model.ozone.ReconServiceModelGenerator
 org.apache.knox.gateway.topology.discovery.cm.model.ozone.SCMServiceModelGenerator
 org.apache.knox.gateway.topology.discovery.cm.model.ozone.OzoneHttpfsServiceModelGenerator
-
-
+org.apache.knox.gateway.topology.discovery.cm.model.opensearch.OpenSearchApiCoordinatorServiceModelGenerator
+org.apache.knox.gateway.topology.discovery.cm.model.opensearch.OpenSearchApiMasterServiceModelGenerator
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/AbstractCMDiscoveryTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/AbstractCMDiscoveryTest.java
index fa2198c..e44ce19 100644
--- a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/AbstractCMDiscoveryTest.java
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/AbstractCMDiscoveryTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.knox.gateway.topology.discovery.cm;
 
+import com.cloudera.api.swagger.model.ApiClusterRef;
 import com.cloudera.api.swagger.model.ApiConfig;
 import com.cloudera.api.swagger.model.ApiConfigList;
 import com.cloudera.api.swagger.model.ApiHostRef;
@@ -30,17 +31,26 @@
 
 public class AbstractCMDiscoveryTest {
   protected static ApiService createApiServiceMock(final String serviceType) {
-    return createApiServiceMock(serviceType + "-1", serviceType);
+    ApiClusterRef clusterRefMock = createApiClusterRefMock("Cluster 1");
+    return createApiServiceMock(serviceType + "-1", serviceType, clusterRefMock);
   }
 
-  protected static ApiService createApiServiceMock(final String serviceName, final String serviceType) {
+  protected static ApiService createApiServiceMock(final String serviceName, final String serviceType, final ApiClusterRef clusterRef) {
     ApiService service = EasyMock.createNiceMock(ApiService.class);
     EasyMock.expect(service.getName()).andReturn(serviceName).anyTimes();
     EasyMock.expect(service.getType()).andReturn(serviceType).anyTimes();
+    EasyMock.expect(service.getClusterRef()).andReturn(clusterRef).anyTimes();
     EasyMock.replay(service);
     return service;
   }
 
+  protected static ApiClusterRef createApiClusterRefMock(final String clusterName) {
+    ApiClusterRef clusterRef = EasyMock.createNiceMock(ApiClusterRef.class);
+    EasyMock.expect(clusterRef.getClusterName()).andReturn(clusterName).anyTimes();
+    EasyMock.replay(clusterRef);
+    return clusterRef;
+  }
+
   protected static ApiServiceConfig createApiServiceConfigMock(Map<String, String> configProps) {
     ApiServiceConfig serviceConfig = EasyMock.createNiceMock(ApiServiceConfig.class);
     EasyMock.expect(serviceConfig.getItems()).andReturn(createMockApiConfigs(configProps)).anyTimes();
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiCoordinatorServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiCoordinatorServiceModelGeneratorTest.java
new file mode 100644
index 0000000..6f5e2d7
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiCoordinatorServiceModelGeneratorTest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you 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.
+ */
+package org.apache.knox.gateway.topology.discovery.cm.model.opensearch;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class OpenSearchApiCoordinatorServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put("ssl_enabled", "true");
+    roleConfig.put("opensearch_http_port", "9202");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return OpenSearchApiCoordinatorServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return OpenSearchApiCoordinatorServiceModelGenerator.OPENSEARCH_COORDINATOR_ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new OpenSearchApiCoordinatorServiceModelGenerator();
+  }
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiMasterServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiMasterServiceModelGeneratorTest.java
new file mode 100644
index 0000000..b33a004
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/opensearch/OpenSearchApiMasterServiceModelGeneratorTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you 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.
+ */
+package org.apache.knox.gateway.topology.discovery.cm.model.opensearch;
+
+import com.cloudera.api.swagger.model.ApiRoleConfig;
+import com.cloudera.api.swagger.model.ApiRoleConfigList;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertTrue;
+
+public class OpenSearchApiMasterServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put("ssl_enabled", "true");
+    roleConfig.put("opensearch_http_port", "9200");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return OpenSearchApiMasterServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return OpenSearchApiMasterServiceModelGenerator.OPENSEARCH_MASTER_ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new OpenSearchApiMasterServiceModelGenerator();
+  }
+
+  @Test
+  public void testSkipGeneratorWhenOpenSearchMaster() {
+    final ApiRoleConfig masterRole = EasyMock.createNiceMock(ApiRoleConfig.class);
+    EasyMock.expect(masterRole.getRoleType()).andReturn(OpenSearchApiMasterServiceModelGenerator.OPENSEARCH_MASTER_ROLE_TYPE).anyTimes();
+    EasyMock.replay(masterRole);
+
+    final ApiRoleConfig coordinatorRole = EasyMock.createNiceMock(ApiRoleConfig.class);
+    EasyMock.expect(coordinatorRole.getRoleType()).andReturn(OpenSearchApiCoordinatorServiceModelGenerator.OPENSEARCH_COORDINATOR_ROLE_TYPE).anyTimes();
+    EasyMock.replay(coordinatorRole);
+
+    final ApiRoleConfigList serviceRoleConfigs = EasyMock.createNiceMock(ApiRoleConfigList.class);
+    EasyMock.expect(serviceRoleConfigs.getItems()).andReturn(Arrays.asList(masterRole, coordinatorRole)).anyTimes();
+    EasyMock.replay(serviceRoleConfigs);
+
+    assertTrue(OpenSearchApiMasterServiceModelGenerator.shouldSkipGeneratorWhenOpenSearchMaster(newGenerator(), serviceRoleConfigs));
+  }
+}
diff --git a/gateway-service-definitions/src/main/resources/services/opensearch/2.18.0/rewrite.xml b/gateway-service-definitions/src/main/resources/services/opensearch/2.18.0/rewrite.xml
new file mode 100644
index 0000000..31b5270
--- /dev/null
+++ b/gateway-service-definitions/src/main/resources/services/opensearch/2.18.0/rewrite.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You 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.
+-->
+<rules>
+    <rule dir="IN" name="OPENSEARCH/opensearch/inbound/root" pattern="*://*:*/**/opensearch/">
+        <rewrite template="{$serviceUrl[OPENSEARCH]}/"/>
+    </rule>
+    <rule dir="IN" name="OPENSEARCH/opensearch/inbound/path" pattern="*://*:*/**/opensearch/{path=**}?{**}">
+        <rewrite template="{$serviceUrl[OPENSEARCH]}/{path=**}?{**}"/>
+    </rule>
+</rules>
+
diff --git a/gateway-service-definitions/src/main/resources/services/opensearch/2.18.0/service.xml b/gateway-service-definitions/src/main/resources/services/opensearch/2.18.0/service.xml
new file mode 100644
index 0000000..f41d3db
--- /dev/null
+++ b/gateway-service-definitions/src/main/resources/services/opensearch/2.18.0/service.xml
@@ -0,0 +1,39 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You 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.
+-->
+<service role="OPENSEARCH" name="opensearch" version="2.18.0">
+    <metadata>
+        <type>API</type>
+        <context>/opensearch</context>
+        <shortDesc>OpenSearch REST API</shortDesc>
+        <description>OpenSearch is an open source, enterprise-grade search and observability suite.</description>
+    </metadata>
+    <routes>
+        <route path="/opensearch"/>
+        <route path="/opensearch/**"/>
+    </routes>
+    <dispatch classname="org.apache.knox.gateway.dispatch.ConfigurableDispatch"
+              ha-classname="org.apache.knox.gateway.ha.dispatch.ConfigurableHADispatch">
+        <param>
+            <name>shouldIncludePrincipalAndGroups</name>
+            <value>true</value>
+        </param>
+        <param>
+            <name>actorIdHeaderName</name>
+            <value>opendistro_security_impersonate_as</value>
+        </param>
+    </dispatch>
+</service>
diff --git a/knox-homepage-ui/home/assets/service-logos/opensearch.png b/knox-homepage-ui/home/assets/service-logos/opensearch.png
new file mode 100644
index 0000000..cea8c4e
--- /dev/null
+++ b/knox-homepage-ui/home/assets/service-logos/opensearch.png
Binary files differ