This is an automated email from the ASF dual-hosted git repository. sk0x50 pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push: new d1885cb IGNITE-14579 Start REST API module. Fixes #219 d1885cb is described below commit d1885cb29a777e7250b598c875e462edda20d40b Author: Kirill Gusakov <kgusa...@gmail.com> AuthorDate: Tue Jul 20 19:19:14 2021 +0300 IGNITE-14579 Start REST API module. Fixes #219 Signed-off-by: Slava Koptilin <slava.kopti...@gmail.com> --- modules/cli/pom.xml | 12 ++ .../org/apache/ignite/cli/ConfigCommandTest.java | 195 +++++++++++++++++++++ .../cli/builtins/config/ConfigurationClient.java | 4 +- .../apache/ignite/cli/IgniteCliInterfaceTest.java | 4 +- .../java/org/apache/ignite/rest/RestModule.java | 15 +- .../apache/ignite/internal/app/IgnitionImpl.java | 14 +- 6 files changed, 232 insertions(+), 12 deletions(-) diff --git a/modules/cli/pom.xml b/modules/cli/pom.xml index 82ad111..909a147 100644 --- a/modules/cli/pom.xml +++ b/modules/cli/pom.xml @@ -113,6 +113,18 @@ <artifactId>micronaut-test-junit5</artifactId> <scope>test</scope> </dependency> + + <dependency> + <groupId>org.apache.ignite</groupId> + <artifactId>ignite-api</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.apache.ignite</groupId> + <artifactId>ignite-runner</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/ConfigCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/ConfigCommandTest.java new file mode 100644 index 0000000..777d2cb --- /dev/null +++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/ConfigCommandTest.java @@ -0,0 +1,195 @@ +/* + * 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.cli; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.nio.file.Path; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.env.Environment; +import org.apache.ignite.app.Ignite; +import org.apache.ignite.app.IgnitionManager; +import org.apache.ignite.cli.spec.IgniteCliSpec; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import picocli.CommandLine; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Integration test for {@code ignite config} commands. + */ +public class ConfigCommandTest extends AbstractCliTest { + /** DI context. */ + private ApplicationContext ctx; + + /** stderr. */ + private ByteArrayOutputStream err; + + /** stdout. */ + private ByteArrayOutputStream out; + + /** Port for REST communication */ + private int restPort; + + /** Network port. */ + private int networkPort; + + /** Ignite node. */ + private Ignite node; + + /** */ + @BeforeEach + private void setup(@TempDir Path workDir) throws IOException { + // TODO: IGNITE-15131 Must be replaced by receiving the actual port configs from the started node. + // This approach still can produce the port, which will be unavailable at the moment of node start. + restPort = getAvailablePort(); + networkPort = getAvailablePort(); + + String cfgStr = "network.port=" + networkPort + "\n" + + "rest.port=" + restPort; + + node = IgnitionManager.start("node1", cfgStr, workDir); + + ctx = ApplicationContext.run(Environment.TEST); + + err = new ByteArrayOutputStream(); + out = new ByteArrayOutputStream(); + } + + /** + * Cleans environment after each test. + * + * @throws Exception If failed to close ignite node or application context. + */ + // TODO: IGNITE-14581 Node must be stopped here. + @AfterEach + private void tearDown() throws Exception { + node.close(); + + ctx.stop(); + } + + /** + * Tests 'config set' and 'config get' commands. + */ + @Test + public void setAndGetWithManualHost() { + int exitCode = cmd(ctx).execute( + "config", + "set", + "--node-endpoint", + "localhost:" + restPort, + "node.metastorageNodes=[\"localhost1\"]"); + + assertEquals(0, exitCode, "The command 'config set' failed [code=" + exitCode + ']'); + assertEquals( + unescapeQuotes("Configuration was updated successfully.\n" + "\n" + + "Use the ignite config get command to view the updated configuration.\n"), + unescapeQuotes(out.toString()), + "The command 'config set' was successfully completed, " + + "but the server response does not match with expected."); + + resetStreams(); + + exitCode = cmd(ctx).execute( + "config", + "get", + "--node-endpoint", + "localhost:" + restPort); + + assertEquals(0, exitCode, "The command 'config get' failed [exitCode=" + exitCode + ']'); + assertEquals( + "\"{\"network\":{\"port\":" + networkPort + ",\"netClusterNodes\":[]}," + + "\"node\":{\"metastorageNodes\":[\"localhost1\"]}," + + "\"rest\":{\"port\":" + restPort + ",\"portRange\":0}}\"", + unescapeQuotes(out.toString()), + "The command 'config get' was successfully completed, " + + "but the server response does not match with expected."); + } + + /** + * Tests partial 'config get' command. + */ + @Test + public void partialGet() { + int exitCode = cmd(ctx).execute( + "config", + "get", + "--node-endpoint", + "localhost:" + restPort, + "--selector", + "network"); + + assertEquals(0, exitCode, "The command 'config get' failed [exitCode=" + exitCode + ']'); + assertEquals( + "\"{\"port\":"+ networkPort + ",\"netClusterNodes\":[]}\"", + unescapeQuotes(out.toString()), + "The command 'config get' was successfully completed, " + + "but the server response does not match with expected."); + } + + /** + * @return Any available port. + * @throws IOException if can't allocate port to open socket. + */ + // TODO: Must be removed after IGNITE-15131. + private int getAvailablePort() throws IOException { + ServerSocket s = new ServerSocket(0); + s.close(); + return s.getLocalPort(); + } + + /** + * @param applicationCtx DI context. + * @return New command line instance. + */ + private CommandLine cmd(ApplicationContext applicationCtx) { + CommandLine.IFactory factory = new CommandFactory(applicationCtx); + + return new CommandLine(IgniteCliSpec.class, factory) + .setErr(new PrintWriter(err, true)) + .setOut(new PrintWriter(out, true)); + } + + /** + * Reset stderr and stdout streams. + */ + private void resetStreams() { + err.reset(); + out.reset(); + } + + /** + * Removes unescaped quotes and new line symbols. + * + * @param input Input string. + * @return New string without new lines and unescaped quotes. + */ + private String unescapeQuotes(String input) { + return input + .replace("\\\"", "\"") + .replaceAll("\\r", "") + .replaceAll("\\n", ""); + } +} diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/builtins/config/ConfigurationClient.java b/modules/cli/src/main/java/org/apache/ignite/cli/builtins/config/ConfigurationClient.java index 25e2092..4a87abe 100644 --- a/modules/cli/src/main/java/org/apache/ignite/cli/builtins/config/ConfigurationClient.java +++ b/modules/cli/src/main/java/org/apache/ignite/cli/builtins/config/ConfigurationClient.java @@ -113,7 +113,7 @@ public class ConfigurationClient { public void set(String host, int port, String rawHoconData, PrintWriter out, ColorScheme cs) { var req = HttpRequest .newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(renderJsonFromHocon(rawHoconData))) + .PUT(HttpRequest.BodyPublishers.ofString(renderJsonFromHocon(rawHoconData))) .header("Content-Type", "application/json") .uri(URI.create("http://" + host + ":" + port + SET_URL)) .build(); @@ -131,7 +131,7 @@ public class ConfigurationClient { throw error("Failed to set configuration", res); } catch (IOException | InterruptedException e) { - throw new IgniteCLIException("Connection issues while trying to send http request"); + throw new IgniteCLIException("Connection issues while trying to send http request", e); } } diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/IgniteCliInterfaceTest.java b/modules/cli/src/test/java/org/apache/ignite/cli/IgniteCliInterfaceTest.java index c24231d..8be4aca 100644 --- a/modules/cli/src/test/java/org/apache/ignite/cli/IgniteCliInterfaceTest.java +++ b/modules/cli/src/test/java/org/apache/ignite/cli/IgniteCliInterfaceTest.java @@ -551,7 +551,7 @@ public class IgniteCliInterfaceTest extends AbstractCliTest { Assertions.assertEquals(0, exitCode); verify(httpClient).send( argThat(r -> "http://localhost:8081/management/v1/configuration/".equals(r.uri().toString()) && - "POST".equals(r.method()) && + "PUT".equals(r.method()) && r.bodyPublisher().get().contentLength() == expSentContent.getBytes().length && "application/json".equals(r.headers().firstValue("Content-Type").get())), any()); @@ -578,7 +578,7 @@ public class IgniteCliInterfaceTest extends AbstractCliTest { Assertions.assertEquals(0, exitCode); verify(httpClient).send( argThat(r -> "http://localhost:8081/management/v1/configuration/".equals(r.uri().toString()) && - "POST".equals(r.method()) && + "PUT".equals(r.method()) && r.bodyPublisher().get().contentLength() == expSentContent.getBytes().length && "application/json".equals(r.headers().firstValue("Content-Type").get())), any()); diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java b/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java index 453d129..b882bfa 100644 --- a/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java +++ b/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java @@ -36,6 +36,7 @@ import org.apache.ignite.configuration.schemas.rest.RestConfiguration; import org.apache.ignite.configuration.schemas.rest.RestView; import org.apache.ignite.configuration.validation.ConfigurationValidationException; import org.apache.ignite.internal.configuration.ConfigurationRegistry; +import org.apache.ignite.lang.IgniteLogger; import org.apache.ignite.rest.netty.RestApiInitializer; import org.apache.ignite.rest.presentation.ConfigurationPresentation; import org.apache.ignite.rest.presentation.json.JsonPresentation; @@ -63,6 +64,9 @@ public class RestModule { /** */ private ConfigurationRegistry sysConf; + /** Ignite logger. */ + private final IgniteLogger LOG = IgniteLogger.forClass(RestModule.class); + /** */ private volatile ConfigurationPresentation<String> presentation; @@ -87,9 +91,8 @@ public class RestModule { /** * @return REST channel future. - * @throws InterruptedException If thread has been interupted during the start. */ - public ChannelFuture start() throws InterruptedException { + public ChannelFuture start() { var router = new Router(); router .get(CONF_URL, (req, resp) -> { @@ -148,7 +151,7 @@ public class RestModule { } /** */ - private ChannelFuture startRestEndpoint(Router router) throws InterruptedException { + private ChannelFuture startRestEndpoint(Router router) { RestView restConfigurationView = sysConf.getConfiguration(RestConfiguration.KEY).value(); int desiredPort = restConfigurationView.port(); @@ -163,6 +166,7 @@ public class RestModule { var hnd = new RestApiInitializer(router); + // TODO: IGNITE-15132 Rest module must reuse netty infrastructure from network module ServerBootstrap b = new ServerBootstrap(); b.option(ChannelOption.SO_BACKLOG, 1024); b.group(parentGrp, childGrp) @@ -170,8 +174,8 @@ public class RestModule { .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(hnd); - for (int portCandidate = desiredPort; portCandidate < desiredPort + portRange; portCandidate++) { - ChannelFuture bindRes = b.bind(portCandidate).await(); + for (int portCandidate = desiredPort; portCandidate <= desiredPort + portRange; portCandidate++) { + ChannelFuture bindRes = b.bind(portCandidate).awaitUninterruptibly(); if (bindRes.isSuccess()) { ch = bindRes.channel(); @@ -179,6 +183,7 @@ public class RestModule { @Override public void operationComplete(ChannelFuture fut) { parentGrp.shutdownGracefully(); childGrp.shutdownGracefully(); + LOG.error("REST component was stopped", fut.cause()); } }); port = portCandidate; diff --git a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgnitionImpl.java b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgnitionImpl.java index bf6f300..e46f510 100644 --- a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgnitionImpl.java +++ b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgnitionImpl.java @@ -34,6 +34,7 @@ import org.apache.ignite.configuration.RootKey; import org.apache.ignite.configuration.annotation.ConfigurationType; import org.apache.ignite.configuration.schemas.network.NetworkConfiguration; import org.apache.ignite.configuration.schemas.network.NetworkView; +import org.apache.ignite.configuration.schemas.rest.RestConfiguration; import org.apache.ignite.configuration.schemas.runner.ClusterConfiguration; import org.apache.ignite.configuration.schemas.runner.NodeConfiguration; import org.apache.ignite.configuration.schemas.table.TablesConfiguration; @@ -58,10 +59,12 @@ import org.apache.ignite.network.ClusterService; import org.apache.ignite.network.MessageSerializationRegistryImpl; import org.apache.ignite.network.NetworkAddress; import org.apache.ignite.network.scalecube.ScaleCubeClusterServiceFactory; +import org.apache.ignite.rest.RestModule; import org.apache.ignite.table.manager.IgniteTables; import org.apache.ignite.utils.IgniteProperties; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.LoggerFactory; /** * Implementation of an entry point for handling grid lifecycle. @@ -152,7 +155,8 @@ public class IgnitionImpl implements Ignition { NetworkConfiguration.KEY, NodeConfiguration.KEY, ClusterConfiguration.KEY, - TablesConfiguration.KEY + TablesConfiguration.KEY, + RestConfiguration.KEY ); List<ConfigurationStorage> cfgStorages = @@ -229,11 +233,15 @@ public class IgnitionImpl implements Ignition { vaultMgr ); - // TODO IGNITE-14579 Start rest manager. - // Deploy all resisted watches cause all components are ready and have registered their listeners. metaStorageMgr.deployWatches(); + RestModule restModule = new RestModule(LoggerFactory.getLogger(RestModule.class.getName())); + + restModule.prepareStart(locConfigurationMgr.configurationRegistry()); + + restModule.start(); + ackSuccessStart(); return new IgniteImpl(distributedTblMgr, vaultMgr);