You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ni...@apache.org on 2023/04/19 09:00:35 UTC
[ignite-extensions] 01/01: IGNITE-15629 One more invoker implemented
This is an automated email from the ASF dual-hosted git repository.
nizhikov pushed a commit to branch IGNITE-15629
in repository https://gitbox.apache.org/repos/asf/ignite-extensions.git
commit 3ebec825c8096950fc285cb9405aae364bd8d3ce
Author: nizhikov <ni...@apache.org>
AuthorDate: Wed Apr 19 12:00:22 2023 +0300
IGNITE-15629 One more invoker implemented
---
modules/openapi-management-ext/pom.xml | 105 ++++++++
.../internal/management/openapi/HeaderFilter.java | 63 +++++
.../management/openapi/ManagementApiServlet.java | 123 +++++++++
.../OpenApiCommandsRegistryInvokerPlugin.java | 288 +++++++++++++++++++++
...enApiCommandsRegistryInvokerPluginProvider.java | 109 ++++++++
.../management/openapi/OpenApiSelfTest.java | 41 +++
pom.xml | 1 +
7 files changed, 730 insertions(+)
diff --git a/modules/openapi-management-ext/pom.xml b/modules/openapi-management-ext/pom.xml
new file mode 100644
index 00000000..4241681e
--- /dev/null
+++ b/modules/openapi-management-ext/pom.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ 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.
+-->
+
+<!--
+ POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-parent-ext-internal</artifactId>
+ <version>1</version>
+ <relativePath>../../parent-internal/pom.xml</relativePath>
+ </parent>
+
+ <properties>
+ <swagger.version>2.2.9</swagger.version>
+ </properties>
+
+ <artifactId>ignite-openapi-management-ext</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ <url>https://ignite.apache.org</url>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-log4j2</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-spring</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-core</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.swagger.core.v3</groupId>
+ <artifactId>swagger-jaxrs2</artifactId>
+ <version>${swagger.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>io.swagger.core.v3</groupId>
+ <artifactId>swagger-jaxrs2-servlet-initializer</artifactId>
+ <version>${swagger.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <version>${jetty.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-servlet</artifactId>
+ <version>${jetty.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>${slf4j.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <version>${slf4j.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/HeaderFilter.java b/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/HeaderFilter.java
new file mode 100644
index 00000000..e45b6006
--- /dev/null
+++ b/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/HeaderFilter.java
@@ -0,0 +1,63 @@
+/*
+ * 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.ignite.internal.management.openapi;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ *
+ */
+public class HeaderFilter implements Filter {
+ /** {@inheritDoc} */
+ @Override public void doFilter(
+ ServletRequest req,
+ ServletResponse resp,
+ FilterChain chain
+ ) throws ServletException, IOException {
+ HttpServletResponse response = (HttpServletResponse) resp;
+ HttpServletRequest request = (HttpServletRequest) req;
+
+ response.setHeader("Access-Control-Allow-Origin", "*");
+ response.setHeader("X-Frame-Options", "SAMEORIGIN");
+ response.setHeader("Access-Control-Allow-Methods", "DELETE, HEAD, GET, OPTIONS, POST, PUT");
+ response.setHeader("Access-Control-Max-Age", "3600");
+ response.setHeader("Access-Control-Allow-Credentials", "true");
+ response.setHeader("Access-Control-Allow-Origin", "*");
+ response.setHeader("Access-Control-Allow-Headers", "Content-Type, api_key, Authorization ");
+
+ chain.doFilter(req, resp);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void init(FilterConfig filterConfig) {
+ // No-op.
+ }
+
+ /** {@inheritDoc} */
+ @Override public void destroy() {
+ // No-op.
+ }
+}
diff --git a/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/ManagementApiServlet.java b/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/ManagementApiServlet.java
new file mode 100644
index 00000000..c07f77d7
--- /dev/null
+++ b/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/ManagementApiServlet.java
@@ -0,0 +1,123 @@
+/*
+ * 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.ignite.internal.management.openapi;
+
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.management.api.Command;
+import org.apache.ignite.internal.management.api.CommandUtils;
+import org.apache.ignite.internal.management.api.CommandsRegistry;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.apache.ignite.internal.management.api.CommandUtils.CMD_WORDS_DELIM;
+import static org.apache.ignite.internal.management.api.CommandUtils.executeAndPrint;
+import static org.apache.ignite.internal.management.api.CommandUtils.formattedName;
+import static org.apache.ignite.internal.management.api.CommandUtils.fromFormattedName;
+import static org.apache.ignite.internal.management.api.CommandUtils.parseVal;
+import static org.apache.ignite.internal.management.openapi.OpenApiCommandsRegistryInvokerPlugin.TEXT_PLAIN;
+
+/** */
+public class ManagementApiServlet extends HttpServlet {
+ /** */
+ private final IgniteEx grid;
+
+ /** */
+ private final String root;
+
+ /** */
+ public ManagementApiServlet(IgniteEx grid, String root) {
+ this.grid = grid;
+ this.root = root;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void doGet(
+ HttpServletRequest req,
+ HttpServletResponse resp
+ ) throws IOException {
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentType(TEXT_PLAIN);
+ resp.setCharacterEncoding("UTF-8");
+
+ String uri = req.getRequestURI();
+
+ if (!uri.startsWith(root))
+ throw new IllegalArgumentException("Wrong URI: " + uri);
+
+ String[] cmdPathAndPosArgs = uri.substring(root.length() + 1).split("/");
+
+ if (cmdPathAndPosArgs.length == 0)
+ throw new IllegalArgumentException("Empty command path: " + uri);
+
+ Command<?, ?, ?> cmd =
+ grid.context().commands().command(fromFormattedName(cmdPathAndPosArgs[0], CMD_WORDS_DELIM));
+
+ if (cmd == null)
+ throw new IllegalArgumentException("Unknown command: " + cmdPathAndPosArgs[0]);
+
+ AtomicInteger i = new AtomicInteger(1);
+
+ while (cmd instanceof CommandsRegistry && i.get() < cmdPathAndPosArgs.length) {
+ Command<?, ?, ?> cmd0 =
+ ((CommandsRegistry)cmd).command(fromFormattedName(cmdPathAndPosArgs[i.get()], CMD_WORDS_DELIM));
+
+ if (cmd0 == null)
+ break;
+
+ cmd = cmd0;
+
+ i.incrementAndGet();
+ }
+
+ parseAndExecute(req, resp, cmdPathAndPosArgs, cmd, i);
+ }
+
+ /** */
+ private <A extends IgniteDataTransferObject> void parseAndExecute(
+ HttpServletRequest req,
+ HttpServletResponse resp,
+ String[] cmdPathAndPosArgs,
+ Command<A, ?, ?> cmd,
+ AtomicInteger i
+ ) throws IOException {
+ try {
+ A arg = CommandUtils.arguments(
+ cmd.args(),
+ (fld, pos) -> i.get() + pos < cmdPathAndPosArgs.length
+ ? parseVal(cmdPathAndPosArgs[i.get() + pos], fld.getType())
+ : null,
+ fld -> {
+ String val = req.getParameter(formattedName(fld.getName(), CMD_WORDS_DELIM));
+
+ return val == null ? null : parseVal(val, fld.getType());
+ }
+ );
+
+ executeAndPrint(grid, cmd, arg, resp.getWriter()::println);
+ }
+ catch (InstantiationException | IllegalAccessException e) {
+ throw new IgniteException(e);
+ }
+ }
+}
diff --git a/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/OpenApiCommandsRegistryInvokerPlugin.java b/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/OpenApiCommandsRegistryInvokerPlugin.java
new file mode 100644
index 00000000..73b51d16
--- /dev/null
+++ b/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/OpenApiCommandsRegistryInvokerPlugin.java
@@ -0,0 +1,288 @@
+/*
+ * 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.ignite.internal.management.openapi;
+
+import io.swagger.v3.jaxrs2.integration.OpenApiServlet;
+import io.swagger.v3.oas.integration.GenericOpenApiContextBuilder;
+import io.swagger.v3.oas.integration.OpenApiContextLocator;
+import io.swagger.v3.oas.integration.SwaggerConfiguration;
+import io.swagger.v3.oas.integration.api.OpenApiContext;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.media.ArraySchema;
+import io.swagger.v3.oas.models.media.BooleanSchema;
+import io.swagger.v3.oas.models.media.Content;
+import io.swagger.v3.oas.models.media.IntegerSchema;
+import io.swagger.v3.oas.models.media.MediaType;
+import io.swagger.v3.oas.models.media.NumberSchema;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.media.StringSchema;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import io.swagger.v3.oas.models.responses.ApiResponse;
+import io.swagger.v3.oas.models.responses.ApiResponses;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.IgniteVersionUtils;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.management.api.Argument;
+import org.apache.ignite.internal.management.api.Command;
+import org.apache.ignite.internal.management.api.CommandWithSubs;
+import org.apache.ignite.internal.management.api.CommandsRegistry;
+import org.apache.ignite.internal.management.api.PositionalArgument;
+import org.apache.ignite.lang.IgniteUuid;
+import org.apache.ignite.plugin.IgnitePlugin;
+import org.apache.ignite.plugin.PluginContext;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+import static io.swagger.v3.oas.integration.api.OpenApiContext.OPENAPI_CONTEXT_ID_KEY;
+import static io.swagger.v3.oas.models.parameters.Parameter.StyleEnum.SIMPLE;
+import static java.util.Collections.singletonList;
+import static javax.servlet.DispatcherType.REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static org.apache.ignite.internal.management.api.CommandUtils.CMD_WORDS_DELIM;
+import static org.apache.ignite.internal.management.api.CommandUtils.PARAM_WORDS_DELIM;
+import static org.apache.ignite.internal.management.api.CommandUtils.formattedName;
+import static org.apache.ignite.internal.management.api.CommandUtils.valueExample;
+import static org.apache.ignite.internal.management.api.CommandUtils.visitCommandParams;
+
+/**
+ *
+ */
+public class OpenApiCommandsRegistryInvokerPlugin implements IgnitePlugin {
+ /** */
+ public static final String API_CTX_ID = "ignite-management-api-ctx";
+
+ /** */
+ public static final String API_ID = "ignite-management-api";
+
+ /** */
+ public static final String INVOKER_BASE_URL = "/management";
+
+ /** */
+ public static final String TEXT_PLAIN = "text/plain";
+
+ /** */
+ private PluginContext ctx;
+
+ /** */
+ private IgniteLogger log;
+
+ /** */
+ private IgniteEx grid;
+
+ /** */
+ private Server srv;
+
+ /** */
+ private final OpenAPI api = new OpenAPI();
+
+ /** */
+ public void context(PluginContext ctx) {
+ this.ctx = ctx;
+ grid = (IgniteEx)ctx.grid();
+ log = ctx.log(OpenApiCommandsRegistryInvokerPlugin.class);
+ }
+
+ /** */
+ public void onIgniteStart() {
+ // TODO: make this configurable.
+ int port = 8080;
+ String host = "localhost";
+ String protocol = "http";
+
+ api.info(new Info()
+ .title("Ignite management API")
+ .description("This endpoint expose Apache Ignite management API methods")
+ .version(IgniteVersionUtils.VER_STR)
+ ).servers(singletonList(
+ new io.swagger.v3.oas.models.servers.Server()
+ .url(protocol + "://" + host + ":" + port + INVOKER_BASE_URL)
+ .description("Ignite node[id=" + grid.localNode().id() + ']')
+ ));
+
+ grid.commands().forEach(cmd -> register(cmd.getKey(), new LinkedList<>(), cmd.getValue()));
+
+ try {
+ SwaggerConfiguration cfg = new SwaggerConfiguration();
+
+ cfg.setId(API_ID);
+ cfg.setOpenAPI(api);
+ cfg.prettyPrint(true);
+
+ OpenApiContext ctx = new GenericOpenApiContextBuilder<>()
+ .ctxId(API_CTX_ID)
+ .openApiConfiguration(cfg)
+ .buildContext(true);
+
+ OpenApiContextLocator.getInstance().putOpenApiContext(API_CTX_ID, ctx);
+
+ srv = new Server();
+
+ ServerConnector connector = new ServerConnector(srv);
+
+ connector.setPort(port);
+ connector.setHost(host);
+
+ srv.addConnector(connector);
+
+ ServletContextHandler handler = new ServletContextHandler();
+
+ ServletHolder apiExposeServlet = new ServletHolder(OpenApiServlet.class);
+
+ apiExposeServlet.setInitParameter(OPENAPI_CONTEXT_ID_KEY, API_CTX_ID);
+
+ handler.addServlet(apiExposeServlet, "/api/*");
+ handler.addServlet(
+ new ServletHolder(new ManagementApiServlet(grid, INVOKER_BASE_URL)),
+ INVOKER_BASE_URL + "/*"
+ );
+ handler.addFilter(HeaderFilter.class, "/*", EnumSet.of(REQUEST));
+
+ srv.setHandler(handler);
+
+ srv.start();
+ }
+ catch (Exception e) {
+ throw new IgniteException(e);
+ }
+ }
+
+ /** */
+ public <A extends IgniteDataTransferObject> void register(String name, List<String> parents, Command<A, ?, ?> cmd) {
+ if (cmd instanceof CommandsRegistry) {
+ parents.add(formattedName(name, CMD_WORDS_DELIM));
+
+ ((Iterable<Map.Entry<String, Command<?, ?, ?>>>)cmd).forEach(
+ cmd0 -> register(cmd0.getKey(), parents, cmd0.getValue())
+ );
+
+ parents.remove(parents.size() - 1);
+
+ if (!((CommandWithSubs)cmd).canBeExecuted())
+ return;
+ }
+
+ StringBuilder path = new StringBuilder();
+
+ for (String parent : parents)
+ path.append('/').append(parent);
+
+ path.append('/').append(formattedName(name, CMD_WORDS_DELIM));
+
+ List<Parameter> params = new ArrayList<>();
+
+ Consumer<Field> fldCnsmr = fld -> params.add(new Parameter()
+ .style(SIMPLE)
+ .in("query")
+ .name(formattedName(fld.getName(), CMD_WORDS_DELIM))
+ .schema(schema(fld.getType()))
+ .description(fld.getAnnotation(Argument.class).description())
+ .example(valueExample(fld))
+ .required(!fld.getAnnotation(Argument.class).optional()));
+
+ // TODO: support oneOf in spec.
+ visitCommandParams(
+ cmd.args(),
+ fld -> {
+ assert !fld.getAnnotation(PositionalArgument.class).optional();
+
+ path.append("/{").append(formattedName(fld.getName(), PARAM_WORDS_DELIM)).append('}');
+
+ params.add(new Parameter()
+ .style(SIMPLE)
+ .in("path")
+ .name(formattedName(fld.getName(), PARAM_WORDS_DELIM))
+ .description(fld.getAnnotation(PositionalArgument.class).description())
+ .example(valueExample(fld)));
+ },
+ fldCnsmr,
+ (optional, flds) -> flds.forEach(fldCnsmr)
+ );
+
+ Content plainText = new Content().addMediaType(TEXT_PLAIN, new MediaType());
+
+ api.path(
+ path.toString(),
+ new PathItem().get(new Operation()
+ .description(cmd.description())
+ .parameters(params)
+ .responses(new ApiResponses()
+ .addApiResponse(Integer.toString(SC_OK), new ApiResponse()
+ .description("Command output")
+ .content(plainText))
+ .addApiResponse(Integer.toString(SC_INTERNAL_SERVER_ERROR), new ApiResponse()
+ .description("Error text")
+ .content(plainText)))
+ ));
+ }
+
+ private Schema<?> schema(Class<?> cls) {
+ if (cls == Float.class || cls == float.class)
+ return new NumberSchema().format("float");
+ else if (cls == Double.class || cls == double.class)
+ return new NumberSchema().format("double");
+ else if (cls == short.class
+ || cls == Short.class
+ || cls == byte.class
+ || cls == Byte.class)
+ return new IntegerSchema().format(null);
+ else if (cls == int.class || cls == Integer.class)
+ return new IntegerSchema();
+ else if (cls == long.class || cls == Long.class)
+ return new IntegerSchema().format("int64");
+ else if (cls == Boolean.class || cls == boolean.class)
+ return new BooleanSchema();
+ else if (cls == String.class)
+ return new StringSchema();
+ else if (cls == UUID.class)
+ return new StringSchema().format("uuid");
+ else if (cls == IgniteUuid.class)
+ return new StringSchema();
+ else if (cls.isArray())
+ return new ArraySchema().items(schema(cls.getComponentType()));
+
+ throw new IllegalArgumentException("Type not supported: " + cls);
+ }
+
+ /** */
+ public void onIgniteStop() {
+ try {
+ if (srv != null)
+ srv.stop();
+ }
+ catch (Exception e) {
+ log.warning("Error stop OpenApi server", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/OpenApiCommandsRegistryInvokerPluginProvider.java b/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/OpenApiCommandsRegistryInvokerPluginProvider.java
new file mode 100644
index 00000000..f887dc94
--- /dev/null
+++ b/modules/openapi-management-ext/src/main/java/org/apache/ignite/internal/management/openapi/OpenApiCommandsRegistryInvokerPluginProvider.java
@@ -0,0 +1,109 @@
+/*
+ * 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.ignite.internal.management.openapi;
+
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.IgniteVersionUtils;
+import org.apache.ignite.plugin.CachePluginContext;
+import org.apache.ignite.plugin.CachePluginProvider;
+import org.apache.ignite.plugin.ExtensionRegistry;
+import org.apache.ignite.plugin.IgnitePlugin;
+import org.apache.ignite.plugin.PluginContext;
+import org.apache.ignite.plugin.PluginProvider;
+import org.apache.ignite.plugin.PluginValidationException;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+/** */
+public class OpenApiCommandsRegistryInvokerPluginProvider implements PluginProvider {
+ /** */
+ private OpenApiCommandsRegistryInvokerPlugin invoker = new OpenApiCommandsRegistryInvokerPlugin();
+
+ /** {@inheritDoc} */
+ @Override public String name() {
+ return "JMX invoker of CommandRegistry";
+ }
+
+ /** {@inheritDoc} */
+ @Override public String version() {
+ return IgniteVersionUtils.VER_STR;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String copyright() {
+ return IgniteVersionUtils.COPYRIGHT;
+ }
+
+ /** {@inheritDoc} */
+ @Override public IgnitePlugin plugin() {
+ return invoker;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void initExtensions(PluginContext ctx, ExtensionRegistry registry) throws IgniteCheckedException {
+ invoker.context(ctx);
+ }
+
+ /** {@inheritDoc} */
+ @Override public CachePluginProvider createCacheProvider(CachePluginContext ctx) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void onIgniteStart() throws IgniteCheckedException {
+ invoker.onIgniteStart();
+ }
+
+ /** {@inheritDoc} */
+ @Override public void onIgniteStop(boolean cancel) {
+ invoker.onIgniteStop();
+ }
+
+ /** {@inheritDoc} */
+ @Override public @Nullable Serializable provideDiscoveryData(UUID nodeId) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void receiveDiscoveryData(UUID nodeId, Serializable data) {
+ // No-op.
+ }
+
+ /** {@inheritDoc} */
+ @Override public void validateNewNode(ClusterNode node) throws PluginValidationException {
+ // No-op.
+ }
+
+ /** {@inheritDoc} */
+ @Override public Object createComponent(PluginContext ctx, Class cls) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void start(PluginContext ctx) throws IgniteCheckedException {
+ // No-op.
+ }
+
+ /** {@inheritDoc} */
+ @Override public void stop(boolean cancel) throws IgniteCheckedException {
+ // No-op.
+ }
+}
diff --git a/modules/openapi-management-ext/src/test/java/org/apache/ignite/internal/management/openapi/OpenApiSelfTest.java b/modules/openapi-management-ext/src/test/java/org/apache/ignite/internal/management/openapi/OpenApiSelfTest.java
new file mode 100644
index 00000000..3babb97c
--- /dev/null
+++ b/modules/openapi-management-ext/src/test/java/org/apache/ignite/internal/management/openapi/OpenApiSelfTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.ignite.internal.management.openapi;
+
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+/**
+ *
+ */
+public class OpenApiSelfTest extends GridCommonAbstractTest {
+ /** {@inheritDoc} */
+ @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+ return super.getConfiguration(igniteInstanceName)
+ .setPluginProviders(new OpenApiCommandsRegistryInvokerPluginProvider());
+ }
+
+ /** */
+ @Test
+ public void testExportOpenApiDefinition() throws Exception {
+ startGrid(0);
+
+ Thread.sleep(3 * 60_000);
+ }
+}
diff --git a/pom.xml b/pom.xml
index c1f8a1a5..37b5a88e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,6 +63,7 @@
<module>modules/cloud-ext</module>
<module>modules/osgi-ext</module>
<module>modules/ssh-ext</module>
+ <module>modules/openapi-management-ext</module>
</modules>
<profiles>