This is an automated email from the ASF dual-hosted git repository. apkhmv 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 e104b42b6e IGNITE-18730 REST API for Deployment Code API (#1733) e104b42b6e is described below commit e104b42b6eeb0c44fa01c869bb15944ee741b434 Author: Mikhail <pochat...@users.noreply.github.com> AuthorDate: Mon Mar 6 16:19:17 2023 +0300 IGNITE-18730 REST API for Deployment Code API (#1733) --- modules/api/build.gradle | 9 + .../org/apache/ignite/deployment/UnitStatus.java | 4 +- .../ignite/deployment/version/UnitVersion.java | 12 +- .../apache/ignite/deployment/version/Version.java | 5 + .../deployment/version/VersionParseException.java | 17 +- .../testframework}/IntegrationTestBase.java | 136 +---------- modules/cli/build.gradle | 5 +- ...onTestBase.java => CliIntegrationTestBase.java} | 167 +------------ .../call/CallInitializedIntegrationTestBase.java | 4 +- ...liCommandTestNotInitializedIntegrationBase.java | 4 +- .../internal/deployunit/DeploymentManagerImpl.java | 1 + modules/rest-api/openapi/openapi.yaml | 224 +++++++++++++++++ .../rest/api/deployment/DeploymentCodeApi.java | 193 +++++++++++++++ .../rest/api/deployment/UnitStatusDto.java | 90 +++++++ .../ignite/internal/rest/constants/HttpCode.java | 2 + .../ignite/internal/rest/constants/MediaType.java | 4 + .../exception/ClusterNotInitializedException.java | 3 - modules/rest/build.gradle | 5 +- .../DeploymentManagementControllerTest.java | 270 +++++++++++++++++++++ .../deployment/CodeDeploymentRestFactory.java} | 40 ++- .../deployment/DeploymentManagementController.java | 99 ++++++++ ...DeploymentUnitAlreadyExistExceptionHandler.java | 44 ++++ .../DeploymentUnitNotFoundExceptionHandler.java | 45 ++++ .../handler/VersionParseExceptionHandler.java | 44 ++++ .../org/apache/ignite/internal/app/IgniteImpl.java | 7 +- 25 files changed, 1091 insertions(+), 343 deletions(-) diff --git a/modules/api/build.gradle b/modules/api/build.gradle index 2fcef62ce9..0cca13ad05 100644 --- a/modules/api/build.gradle +++ b/modules/api/build.gradle @@ -18,6 +18,7 @@ apply from: "$rootDir/buildscripts/java-core.gradle" apply from: "$rootDir/buildscripts/publishing.gradle" apply from: "$rootDir/buildscripts/java-junit5.gradle" +apply from: "$rootDir/buildscripts/java-test-fixtures.gradle" dependencies { @@ -30,6 +31,14 @@ dependencies { testImplementation libs.hamcrest.optional testImplementation libs.archunit.core testImplementation libs.archunit.junit5 + + testFixturesAnnotationProcessor project(":ignite-configuration-annotation-processor") + testFixturesAnnotationProcessor libs.micronaut.inject.annotation.processor + testFixturesImplementation project(":ignite-core") + testFixturesImplementation testFixtures(project(":ignite-core")) + testFixturesImplementation project(":ignite-configuration") + testFixturesImplementation libs.hamcrest.core + testFixturesImplementation libs.micronaut.junit5 } description = 'ignite-api' diff --git a/modules/api/src/main/java/org/apache/ignite/deployment/UnitStatus.java b/modules/api/src/main/java/org/apache/ignite/deployment/UnitStatus.java index 771a0fd8db..a9ef4f5a4c 100644 --- a/modules/api/src/main/java/org/apache/ignite/deployment/UnitStatus.java +++ b/modules/api/src/main/java/org/apache/ignite/deployment/UnitStatus.java @@ -18,11 +18,11 @@ package org.apache.ignite.deployment; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.TreeMap; import org.apache.ignite.deployment.version.Version; import org.apache.ignite.internal.tostring.S; @@ -123,7 +123,7 @@ public class UnitStatus { public static class UnitStatusBuilder { private final String id; - private final Map<Version, List<String>> versionToConsistentIds = new HashMap<>(); + private final Map<Version, List<String>> versionToConsistentIds = new TreeMap<>(Version::compareTo); /** * Constructor. diff --git a/modules/api/src/main/java/org/apache/ignite/deployment/version/UnitVersion.java b/modules/api/src/main/java/org/apache/ignite/deployment/version/UnitVersion.java index 02b2df683d..08bfd8d9e4 100644 --- a/modules/api/src/main/java/org/apache/ignite/deployment/version/UnitVersion.java +++ b/modules/api/src/main/java/org/apache/ignite/deployment/version/UnitVersion.java @@ -49,16 +49,16 @@ public class UnitVersion implements Version { /** * Parse string representation of version to {@link UnitVersion} if possible. * - * @param s String representation of version. + * @param rawVersion String representation of version. * @return Instance of {@link UnitVersion}. * @throws VersionParseException in case when string is not required {@link UnitVersion} format. */ - public static UnitVersion parse(String s) { - Objects.requireNonNull(s); + public static UnitVersion parse(String rawVersion) { + Objects.requireNonNull(rawVersion); try { - String[] split = s.split("\\.", -1); + String[] split = rawVersion.split("\\.", -1); if (split.length > 3 || split.length == 0) { - throw new VersionParseException("Invalid version format"); + throw new VersionParseException(rawVersion, "Invalid version format"); } short major = Short.parseShort(split[0]); @@ -67,7 +67,7 @@ public class UnitVersion implements Version { return new UnitVersion(major, minor, patch); } catch (NumberFormatException e) { - throw new VersionParseException(e); + throw new VersionParseException(rawVersion, e); } } diff --git a/modules/api/src/main/java/org/apache/ignite/deployment/version/Version.java b/modules/api/src/main/java/org/apache/ignite/deployment/version/Version.java index f9a4e29847..9bb09073a4 100644 --- a/modules/api/src/main/java/org/apache/ignite/deployment/version/Version.java +++ b/modules/api/src/main/java/org/apache/ignite/deployment/version/Version.java @@ -47,6 +47,11 @@ public interface Version extends Comparable<Version> { } return 1; } + + @Override + public String toString() { + return render(); + } }; /** diff --git a/modules/api/src/main/java/org/apache/ignite/deployment/version/VersionParseException.java b/modules/api/src/main/java/org/apache/ignite/deployment/version/VersionParseException.java index 717035a9c0..9ab83eaddd 100644 --- a/modules/api/src/main/java/org/apache/ignite/deployment/version/VersionParseException.java +++ b/modules/api/src/main/java/org/apache/ignite/deployment/version/VersionParseException.java @@ -21,20 +21,16 @@ package org.apache.ignite.deployment.version; * Throws when {@link Version} of deployment unit not parsable. */ public class VersionParseException extends RuntimeException { - /** - * Constructor. - */ - public VersionParseException() { - - } + private final String rawVersion; /** * Constructor. * * @param cause Cause error. */ - public VersionParseException(Throwable cause) { + public VersionParseException(String rawVersion, Throwable cause) { super(cause); + this.rawVersion = rawVersion; } /** @@ -42,7 +38,12 @@ public class VersionParseException extends RuntimeException { * * @param message Error message. */ - public VersionParseException(String message) { + public VersionParseException(String rawVersion, String message) { super(message); + this.rawVersion = rawVersion; + } + + public String getRawVersion() { + return rawVersion; } } diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/IntegrationTestBase.java b/modules/api/src/testFixtures/java/org/apache/ignite/internal/testframework/IntegrationTestBase.java similarity index 57% copy from modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/IntegrationTestBase.java copy to modules/api/src/testFixtures/java/org/apache/ignite/internal/testframework/IntegrationTestBase.java index 00c1997818..ffd8c9b06a 100644 --- a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/IntegrationTestBase.java +++ b/modules/api/src/testFixtures/java/org/apache/ignite/internal/testframework/IntegrationTestBase.java @@ -15,46 +15,28 @@ * limitations under the License. */ -package org.apache.ignite.internal.cli; +package org.apache.ignite.internal.testframework; import static java.util.stream.Collectors.toList; -import static org.apache.ignite.internal.testframework.IgniteTestUtils.await; import static org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName; import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; import static org.hamcrest.MatcherAssert.assertThat; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; -import java.io.PrintWriter; -import java.io.Writer; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; import java.util.stream.IntStream; import org.apache.ignite.Ignite; import org.apache.ignite.IgnitionManager; import org.apache.ignite.InitParameters; -import org.apache.ignite.internal.app.IgniteImpl; import org.apache.ignite.internal.logger.IgniteLogger; import org.apache.ignite.internal.logger.Loggers; -import org.apache.ignite.internal.sql.engine.AsyncCursor; -import org.apache.ignite.internal.sql.engine.AsyncCursor.BatchedResult; -import org.apache.ignite.internal.sql.engine.QueryContext; -import org.apache.ignite.internal.sql.engine.QueryProperty; -import org.apache.ignite.internal.sql.engine.property.PropertiesHolder; -import org.apache.ignite.internal.sql.engine.session.SessionId; -import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest; -import org.apache.ignite.internal.testframework.WorkDirectory; -import org.apache.ignite.internal.testframework.WorkDirectoryExtension; import org.apache.ignite.internal.util.IgniteUtils; import org.apache.ignite.lang.IgniteStringFormatter; -import org.apache.ignite.table.Table; -import org.apache.ignite.tx.Transaction; -import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInstance; @@ -63,9 +45,9 @@ import org.junit.jupiter.api.extension.ExtendWith; /** * Integration test base. Setups ignite cluster per test class and provides useful fixtures and assertions. */ -@ExtendWith(WorkDirectoryExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @MicronautTest(rebuildContext = true) +@ExtendWith(WorkDirectoryExtension.class) public class IntegrationTestBase extends BaseIgniteAbstractTest { /** Correct ignite cluster url. */ protected static final String NODE_URL = "http://localhost:10300"; @@ -79,10 +61,6 @@ public class IntegrationTestBase extends BaseIgniteAbstractTest { /** Node name to its configuration map.*/ protected static final Map<String, String> NODE_CONFIGS = new HashMap<>(); - /** Timeout should be big enough to prevent premature session expiration. */ - - private static final long SESSION_IDLE_TIMEOUT = TimeUnit.SECONDS.toMillis(60); - private static final int DEFAULT_NODES_COUNT = 3; private static final IgniteLogger LOG = Loggers.forClass(IntegrationTestBase.class); @@ -102,112 +80,12 @@ public class IntegrationTestBase extends BaseIgniteAbstractTest { + " }\n" + "}"; - /** Template for node bootstrap config with Scalecube and Logical Topology settings for fast failure detection. */ - protected static final String FAST_FAILURE_DETECTION_NODE_BOOTSTRAP_CFG_TEMPLATE = "{\n" - + " network: {\n" - + " port: {},\n" - + " nodeFinder: {\n" - + " netClusterNodes: [ {} ]\n" - + " },\n" - + " membership: {\n" - + " membershipSyncInterval: 1000,\n" - + " failurePingInterval: 500,\n" - + " scaleCube: {\n" - + " membershipSuspicionMultiplier: 1,\n" - + " failurePingRequestMembers: 1,\n" - + " gossipInterval: 10\n" - + " },\n" - + " }\n" - + " }," - + " cluster.failoverTimeout: 100\n" - + "}"; - /** Futures that are going to be completed when all nodes are started and the cluster is initialized. */ private static List<CompletableFuture<Ignite>> futures = new ArrayList<>(); /** Work directory. */ @WorkDirectory - private static Path WORK_DIR; - - protected static void createAndPopulateTable() { - sql("CREATE TABLE person ( id INT PRIMARY KEY, name VARCHAR, salary DOUBLE)"); - - int idx = 0; - - for (Object[] args : new Object[][]{ - {idx++, "Igor", 10d}, - {idx++, null, 15d}, - {idx++, "Ilya", 15d}, - {idx++, "Roma", 10d}, - {idx, "Roma", 10d} - }) { - sql("INSERT INTO person(id, name, salary) VALUES (?, ?, ?)", args); - } - } - - protected static List<List<Object>> sql(String sql, Object... args) { - return sql(null, sql, args); - } - - protected static List<List<Object>> sql(@Nullable Transaction tx, String sql, Object... args) { - var queryEngine = ((IgniteImpl) CLUSTER_NODES.get(0)).queryEngine(); - - SessionId sessionId = queryEngine.createSession(SESSION_IDLE_TIMEOUT, PropertiesHolder.fromMap( - Map.of(QueryProperty.DEFAULT_SCHEMA, "PUBLIC") - )); - - try { - var context = tx != null ? QueryContext.of(tx) : QueryContext.of(); - - return getAllFromCursor( - await(queryEngine.querySingleAsync(sessionId, context, sql, args)) - ); - } finally { - queryEngine.closeSession(sessionId); - } - } - - private static <T> List<T> getAllFromCursor(AsyncCursor<T> cur) { - List<T> res = new ArrayList<>(); - int batchSize = 256; - - var consumer = new Consumer<BatchedResult<T>>() { - @Override - public void accept(BatchedResult<T> br) { - res.addAll(br.items()); - - if (br.hasMore()) { - cur.requestNextAsync(batchSize).thenAccept(this); - } - } - }; - - await(cur.requestNextAsync(batchSize).thenAccept(consumer)); - await(cur.closeAsync()); - - return res; - } - - protected static PrintWriter output(List<Character> buffer) { - return new PrintWriter(new Writer() { - @Override - public void write(char[] cbuf, int off, int len) { - for (int i = off; i < off + len; i++) { - buffer.add(cbuf[i]); - } - } - - @Override - public void flush() { - - } - - @Override - public void close() { - - } - }); - } + protected static Path WORK_DIR; /** * Before all. @@ -289,18 +167,10 @@ public class IntegrationTestBase extends BaseIgniteAbstractTest { CLUSTER_NODE_NAMES.add(nodeName); } - /** Drops all visible tables. */ - protected void dropAllTables() { - for (Table t : CLUSTER_NODES.get(0).tables().tables()) { - sql("DROP TABLE " + t.name()); - } - } - /** * Invokes before the test will start. * * @param testInfo Test information object. - * @throws Exception If failed. */ public void setUp(TestInfo testInfo) throws Exception { setupBase(testInfo, WORK_DIR); diff --git a/modules/cli/build.gradle b/modules/cli/build.gradle index 59d5ee5d85..04fd0784f1 100644 --- a/modules/cli/build.gradle +++ b/modules/cli/build.gradle @@ -95,8 +95,9 @@ dependencies { integrationTestImplementation project(':ignite-runner') integrationTestImplementation project(':ignite-schema') integrationTestImplementation project(':ignite-sql-engine') - integrationTestImplementation(testFixtures(project(":ignite-core"))) - integrationTestImplementation(testFixtures(project(":ignite-schema"))) + integrationTestImplementation testFixtures(project(":ignite-core")) + integrationTestImplementation testFixtures(project(":ignite-schema")) + integrationTestImplementation testFixtures(project(":ignite-api")) integrationTestImplementation libs.jetbrains.annotations integrationTestImplementation libs.micronaut.picocli integrationTestImplementation libs.mock.server.netty diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/IntegrationTestBase.java b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/CliIntegrationTestBase.java similarity index 50% rename from modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/IntegrationTestBase.java rename to modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/CliIntegrationTestBase.java index 00c1997818..0db0d1e8e6 100644 --- a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/IntegrationTestBase.java +++ b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/CliIntegrationTestBase.java @@ -17,46 +17,28 @@ package org.apache.ignite.internal.cli; -import static java.util.stream.Collectors.toList; import static org.apache.ignite.internal.testframework.IgniteTestUtils.await; -import static org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName; -import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; -import static org.hamcrest.MatcherAssert.assertThat; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import java.io.PrintWriter; import java.io.Writer; -import java.nio.file.Path; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import java.util.stream.IntStream; -import org.apache.ignite.Ignite; -import org.apache.ignite.IgnitionManager; -import org.apache.ignite.InitParameters; import org.apache.ignite.internal.app.IgniteImpl; -import org.apache.ignite.internal.logger.IgniteLogger; -import org.apache.ignite.internal.logger.Loggers; import org.apache.ignite.internal.sql.engine.AsyncCursor; import org.apache.ignite.internal.sql.engine.AsyncCursor.BatchedResult; import org.apache.ignite.internal.sql.engine.QueryContext; import org.apache.ignite.internal.sql.engine.QueryProperty; import org.apache.ignite.internal.sql.engine.property.PropertiesHolder; import org.apache.ignite.internal.sql.engine.session.SessionId; -import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest; -import org.apache.ignite.internal.testframework.WorkDirectory; +import org.apache.ignite.internal.testframework.IntegrationTestBase; import org.apache.ignite.internal.testframework.WorkDirectoryExtension; -import org.apache.ignite.internal.util.IgniteUtils; -import org.apache.ignite.lang.IgniteStringFormatter; import org.apache.ignite.table.Table; import org.apache.ignite.tx.Transaction; import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -66,43 +48,16 @@ import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(WorkDirectoryExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @MicronautTest(rebuildContext = true) -public class IntegrationTestBase extends BaseIgniteAbstractTest { - /** Correct ignite cluster url. */ - protected static final String NODE_URL = "http://localhost:10300"; - - /** Cluster nodes. */ - protected static final List<Ignite> CLUSTER_NODES = new ArrayList<>(); - - /** Cluster node names. */ - protected static final List<String> CLUSTER_NODE_NAMES = new ArrayList<>(); - - /** Node name to its configuration map.*/ - protected static final Map<String, String> NODE_CONFIGS = new HashMap<>(); - - /** Timeout should be big enough to prevent premature session expiration. */ - +public class CliIntegrationTestBase extends IntegrationTestBase { + /** + * Timeout should be big enough to prevent premature session expiration. + */ private static final long SESSION_IDLE_TIMEOUT = TimeUnit.SECONDS.toMillis(60); - private static final int DEFAULT_NODES_COUNT = 3; - - private static final IgniteLogger LOG = Loggers.forClass(IntegrationTestBase.class); - - /** Base port number. */ - - private static final int BASE_PORT = 3344; - - /** Nodes bootstrap configuration pattern. */ - private static final String NODE_BOOTSTRAP_CFG = "{\n" - + " network: {\n" - + " port:{},\n" - + " portRange: 5,\n" - + " nodeFinder:{\n" - + " netClusterNodes: [ {} ]\n" - + " }\n" - + " }\n" - + "}"; - /** Template for node bootstrap config with Scalecube and Logical Topology settings for fast failure detection. */ + /** + * Template for node bootstrap config with Scalecube and Logical Topology settings for fast failure detection. + */ protected static final String FAST_FAILURE_DETECTION_NODE_BOOTSTRAP_CFG_TEMPLATE = "{\n" + " network: {\n" + " port: {},\n" @@ -122,12 +77,6 @@ public class IntegrationTestBase extends BaseIgniteAbstractTest { + " cluster.failoverTimeout: 100\n" + "}"; - /** Futures that are going to be completed when all nodes are started and the cluster is initialized. */ - private static List<CompletableFuture<Ignite>> futures = new ArrayList<>(); - /** Work directory. */ - - @WorkDirectory - private static Path WORK_DIR; protected static void createAndPopulateTable() { sql("CREATE TABLE person ( id INT PRIMARY KEY, name VARCHAR, salary DOUBLE)"); @@ -209,111 +158,11 @@ public class IntegrationTestBase extends BaseIgniteAbstractTest { }); } - /** - * Before all. - * - * @param testInfo Test information object. - */ - protected void startNodes(TestInfo testInfo) { - String connectNodeAddr = "\"localhost:" + BASE_PORT + '\"'; - - futures = IntStream.range(0, nodes()) - .mapToObj(i -> { - String nodeName = testNodeName(testInfo, i); - CLUSTER_NODE_NAMES.add(nodeName); - - String config = IgniteStringFormatter.format(nodeBootstrapConfigTemplate(), BASE_PORT + i, connectNodeAddr); - - NODE_CONFIGS.put(nodeName, config); - - return IgnitionManager.start(nodeName, config, WORK_DIR.resolve(nodeName)); - }) - .collect(toList()); - } - - protected String nodeBootstrapConfigTemplate() { - return NODE_BOOTSTRAP_CFG; - } - - protected void initializeCluster(String metaStorageNodeName) { - InitParameters initParameters = InitParameters.builder() - .destinationNodeName(metaStorageNodeName) - .metaStorageNodeNames(List.of(metaStorageNodeName)) - .clusterName("cluster") - .build(); - - IgnitionManager.init(initParameters); - - for (CompletableFuture<Ignite> future : futures) { - assertThat(future, willCompleteSuccessfully()); - - CLUSTER_NODES.add(future.join()); - } - } - - /** - * Get a count of nodes in the Ignite cluster. - * - * @return Count of nodes. - */ - protected int nodes() { - return DEFAULT_NODES_COUNT; - } - - /** - * After all. - */ - protected void stopNodes(TestInfo testInfo) throws Exception { - LOG.info("Start tearDown()"); - - CLUSTER_NODES.clear(); - CLUSTER_NODE_NAMES.clear(); - - List<AutoCloseable> closeables = IntStream.range(0, nodes()) - .mapToObj(i -> testNodeName(testInfo, i)) - .map(nodeName -> (AutoCloseable) () -> IgnitionManager.stop(nodeName)) - .collect(toList()); - - IgniteUtils.closeAll(closeables); - - LOG.info("End tearDown()"); - } - - protected void stopNode(String nodeName) { - IgnitionManager.stop(nodeName); - CLUSTER_NODE_NAMES.remove(nodeName); - } - - protected void startNode(String nodeName) { - IgnitionManager.start(nodeName, NODE_CONFIGS.get(nodeName), WORK_DIR.resolve(nodeName)); - CLUSTER_NODE_NAMES.add(nodeName); - } - /** Drops all visible tables. */ protected void dropAllTables() { for (Table t : CLUSTER_NODES.get(0).tables().tables()) { sql("DROP TABLE " + t.name()); } } - - /** - * Invokes before the test will start. - * - * @param testInfo Test information object. - * @throws Exception If failed. - */ - public void setUp(TestInfo testInfo) throws Exception { - setupBase(testInfo, WORK_DIR); - } - - /** - * Invokes after the test has finished. - * - * @param testInfo Test information object. - */ - @AfterEach - public void tearDown(TestInfo testInfo) { - tearDownBase(testInfo); - } } diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/call/CallInitializedIntegrationTestBase.java b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/call/CallInitializedIntegrationTestBase.java index d3396dcfe2..dd37705261 100644 --- a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/call/CallInitializedIntegrationTestBase.java +++ b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/call/CallInitializedIntegrationTestBase.java @@ -19,7 +19,7 @@ package org.apache.ignite.internal.cli.call; import static org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName; -import org.apache.ignite.internal.cli.IntegrationTestBase; +import org.apache.ignite.internal.cli.CliIntegrationTestBase; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.TestInfo; @@ -27,7 +27,7 @@ import org.junit.jupiter.api.TestInfo; /** * Base class for call integration tests that needs initialized ignite cluster. Contains common methods and useful assertions. */ -public class CallInitializedIntegrationTestBase extends IntegrationTestBase { +public class CallInitializedIntegrationTestBase extends CliIntegrationTestBase { @BeforeAll void beforeAll(TestInfo testInfo) { startNodes(testInfo); diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/CliCommandTestNotInitializedIntegrationBase.java b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/CliCommandTestNotInitializedIntegrationBase.java index deca8458a8..7b6d7c14e0 100644 --- a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/CliCommandTestNotInitializedIntegrationBase.java +++ b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/CliCommandTestNotInitializedIntegrationBase.java @@ -24,7 +24,7 @@ import io.micronaut.context.ApplicationContext; import jakarta.inject.Inject; import java.io.PrintWriter; import java.io.StringWriter; -import org.apache.ignite.internal.cli.IntegrationTestBase; +import org.apache.ignite.internal.cli.CliIntegrationTestBase; import org.apache.ignite.internal.cli.commands.cliconfig.TestConfigManagerHelper; import org.apache.ignite.internal.cli.commands.cliconfig.TestConfigManagerProvider; import org.apache.ignite.internal.cli.commands.node.NodeNameOrUrl; @@ -46,7 +46,7 @@ import picocli.CommandLine; * Integration test base for cli commands. Setup commands, ignite cluster, and provides useful fixtures and assertions. Note: ignite cluster * won't be initialized. If you want to use initialized cluster use {@link CliCommandTestInitializedIntegrationBase}. */ -public class CliCommandTestNotInitializedIntegrationBase extends IntegrationTestBase { +public class CliCommandTestNotInitializedIntegrationBase extends CliIntegrationTestBase { /** Correct ignite jdbc url. */ protected static final String JDBC_URL = "jdbc:ignite:thin://127.0.0.1:10800"; diff --git a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/DeploymentManagerImpl.java b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/DeploymentManagerImpl.java index 2c2869a503..683f4f6878 100644 --- a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/DeploymentManagerImpl.java +++ b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/DeploymentManagerImpl.java @@ -300,6 +300,7 @@ public class DeploymentManagerImpl implements IgniteDeployment, IgniteComponent @Override public void onComplete() { + Collections.sort(list); result.complete(list); } }); diff --git a/modules/rest-api/openapi/openapi.yaml b/modules/rest-api/openapi/openapi.yaml index cb57668eea..e7b2305b49 100644 --- a/modules/rest-api/openapi/openapi.yaml +++ b/modules/rest-api/openapi/openapi.yaml @@ -318,6 +318,215 @@ paths: application/problem+json: schema: $ref: '#/components/schemas/Problem' + /management/v1/deployment/units: + get: + tags: + - deployment + description: All units statutes. + operationId: units + parameters: [] + responses: + "200": + description: All statutes returned successful. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnitStatus' + "500": + description: Internal error. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + post: + tags: + - deployment + description: Deploy provided unit to the cluster. + operationId: deployUnit + parameters: [] + requestBody: + content: + multipart/form-data: + schema: + required: + - unitContent + - unitId + type: object + properties: + unitId: + required: + - "true" + type: string + unitVersion: + type: string + unitContent: + required: + - "true" + type: string + format: binary + required: true + responses: + "200": + description: Unit deployed successfully. + content: + application/json: + schema: + type: boolean + "409": + description: Unit with same identifier and version already deployed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + "500": + description: Internal error. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + /management/v1/deployment/units/{unitId}: + delete: + tags: + - deployment + description: Undeploy latest unit with provided unitId. + operationId: undeployLatestUnit + parameters: + - name: unitId + in: path + required: true + schema: + required: + - "true" + type: string + responses: + "200": + description: Unit undeployed successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/Void' + "404": + description: Unit with provided identifier and version doesn't exist. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + "500": + description: Internal error. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + /management/v1/deployment/units/{unitId}/status: + get: + tags: + - deployment + description: Status of unit with provided identifier. + operationId: status + parameters: + - name: unitId + in: path + required: true + schema: + required: + - "true" + type: string + responses: + "200": + description: Status returned successful. + content: + application/json: + schema: + $ref: '#/components/schemas/UnitStatus' + "404": + description: Unit with provided identifier doesn't exist. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + "500": + description: Internal error. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + /management/v1/deployment/units/{unitId}/versions: + get: + tags: + - deployment + description: All versions of unit with provided unit identifier. + operationId: versions + parameters: + - name: unitId + in: path + required: true + schema: + required: + - "true" + type: string + responses: + "200": + description: Versions returned successful. + content: + application/json: + schema: + type: array + items: + type: string + "404": + description: Unit with provided identifier doesn't exist. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + "500": + description: Internal error. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + /management/v1/deployment/units/{unitId}/{unitVersion}: + delete: + tags: + - deployment + description: Undeploy unit with provided unitId and unitVersion. + operationId: undeployUnit + parameters: + - name: unitId + in: path + required: true + schema: + required: + - "true" + type: string + - name: unitVersion + in: path + required: true + schema: + required: + - "true" + type: string + responses: + "200": + description: Unit undeployed successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/Void' + "404": + description: Unit with provided identifier and version doesn't exist. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + "500": + description: Internal error. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' /management/v1/metric/node: get: tags: @@ -635,5 +844,20 @@ components: - STARTING - STARTED - STOPPING + UnitStatus: + required: + - id + - versionToConsistentIds + type: object + properties: + id: + type: string + versionToConsistentIds: + type: object + additionalProperties: + type: array + items: + type: string + description: Unit status. Void: type: object diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/DeploymentCodeApi.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/DeploymentCodeApi.java new file mode 100644 index 0000000000..67bdd5d3bb --- /dev/null +++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/DeploymentCodeApi.java @@ -0,0 +1,193 @@ +/* + * 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.rest.api.deployment; + +import static org.apache.ignite.internal.rest.constants.MediaType.APPLICATION_JSON; +import static org.apache.ignite.internal.rest.constants.MediaType.PROBLEM_JSON; + +import io.micronaut.http.annotation.Consumes; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.multipart.CompletedFileUpload; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import org.apache.ignite.internal.rest.api.Problem; +import org.apache.ignite.internal.rest.constants.MediaType; + +/** + * REST endpoint allows to deployment code service. + */ +@Controller("/management/v1/deployment/") +@Tag(name = "deployment") +public interface DeploymentCodeApi { + + /** + * Deploy unit REST method. + */ + @Operation(operationId = "deployUnit", description = "Deploy provided unit to the cluster.") + @ApiResponse(responseCode = "200", description = "Unit deployed successfully.", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(type = "boolean")) + ) + @ApiResponse(responseCode = "409", description = "Unit with same identifier and version already deployed.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class)) + ) + @ApiResponse(responseCode = "500", description = "Internal error.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class)) + ) + @Consumes(MediaType.FORM_DATA) + @Produces({ + APPLICATION_JSON, + PROBLEM_JSON + }) + @Post("units") + CompletableFuture<Boolean> deploy( + @Schema(name = "unitId", required = true) String unitId, + @Schema(name = "unitVersion") String unitVersion, + @Schema(name = "unitContent", required = true) CompletedFileUpload unitContent); + + /** + * Undeploy unit REST method. + */ + @Operation(operationId = "undeployUnit", description = "Undeploy unit with provided unitId and unitVersion.") + @ApiResponse(responseCode = "200", + description = "Unit undeployed successfully.", + //DO NOT Remove redundant parameter. It will BREAK generated spec. + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Void.class)) + ) + @ApiResponse(responseCode = "404", + description = "Unit with provided identifier and version doesn't exist.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class)) + ) + @ApiResponse(responseCode = "500", + description = "Internal error.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class)) + ) + @Consumes(APPLICATION_JSON) + @Produces({ + APPLICATION_JSON, + PROBLEM_JSON + }) + @Delete("units/{unitId}/{unitVersion}") + CompletableFuture<Void> undeploy( + @PathVariable("unitId") @Schema(name = "unitId", required = true) String unitId, + @PathVariable("unitVersion") @Schema(name = "unitVersion", required = true) String unitVersion); + + /** + * Undeploy latest unit REST method. + */ + @Operation(operationId = "undeployLatestUnit", description = "Undeploy latest unit with provided unitId.") + @ApiResponse(responseCode = "200", + description = "Unit undeployed successfully.", + //DO NOT Remove redundant parameter. It will BREAK generated spec. + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Void.class)) + ) + @ApiResponse(responseCode = "404", + description = "Unit with provided identifier and version doesn't exist.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class)) + ) + @ApiResponse(responseCode = "500", + description = "Internal error.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class)) + ) + @Consumes(APPLICATION_JSON) + @Produces({ + APPLICATION_JSON, + PROBLEM_JSON + }) + @Delete("units/{unitId}") + CompletableFuture<Void> undeploy( + @PathVariable("unitId") @Schema(name = "unitId", required = true) String unitId); + + /** + * All units status REST method. + */ + @Operation(operationId = "units", description = "All units statutes.") + @ApiResponse(responseCode = "200", + description = "All statutes returned successful.", + content = @Content(mediaType = APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = UnitStatusDto.class))) + ) + @ApiResponse(responseCode = "500", + description = "Internal error.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class)) + ) + @Consumes(MediaType.TEXT_PLAIN) + @Produces({ + APPLICATION_JSON, + PROBLEM_JSON + }) + @Get("units") + CompletableFuture<Collection<UnitStatusDto>> units(); + + /** + * Versions of unit REST method. + */ + @Operation(operationId = "versions", description = "All versions of unit with provided unit identifier.") + @ApiResponse(responseCode = "200", + description = "Versions returned successful.", + content = @Content(mediaType = APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = String.class))) + ) + @ApiResponse(responseCode = "404", + description = "Unit with provided identifier doesn't exist.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class)) + ) + @ApiResponse(responseCode = "500", description = "Internal error.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class))) + @Consumes(APPLICATION_JSON) + @Produces({ + APPLICATION_JSON, + PROBLEM_JSON + }) + @Get("units/{unitId}/versions") + CompletableFuture<Collection<String>> versions( + @PathVariable("unitId") @Schema(name = "unitId", required = true) String unitId); + + /** + * Unit status REST method. + */ + @Operation(operationId = "status", description = "Status of unit with provided identifier.") + @ApiResponse(responseCode = "200", + description = "Status returned successful.", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = UnitStatusDto.class)) + ) + @ApiResponse(responseCode = "404", + description = "Unit with provided identifier doesn't exist.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class)) + ) + @ApiResponse(responseCode = "500", + description = "Internal error.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class)) + ) + @Consumes(APPLICATION_JSON) + @Produces({ + APPLICATION_JSON, + PROBLEM_JSON + }) + @Get("units/{unitId}/status") + CompletableFuture<UnitStatusDto> status( + @PathVariable("unitId") @Schema(name = "unitId", required = true) String unitId); +} diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/UnitStatusDto.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/UnitStatusDto.java new file mode 100644 index 0000000000..29af87b47b --- /dev/null +++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/UnitStatusDto.java @@ -0,0 +1,90 @@ +/* + * 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.rest.api.deployment; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.ignite.deployment.UnitStatus; +import org.apache.ignite.deployment.version.Version; + +/** + * DTO of {@link UnitStatus}. + */ +@Schema(name = "UnitStatus", description = "Unit status.") +public class UnitStatusDto { + + /** + * Unit identifier. + */ + private final String id; + + /** + * Map from existing unit version to list of nodes consistent ids where unit deployed. + */ + private final Map<String, List<String>> versionToConsistentIds; + + @JsonCreator + public UnitStatusDto(@JsonProperty("id") String id, + @JsonProperty("versionToNodes") Map<String, List<String>> versionToConsistentIds) { + this.id = id; + this.versionToConsistentIds = versionToConsistentIds; + } + + /** + * Returns unit identifier. + * + * @return Unit identifier. + */ + @JsonGetter("id") + public String id() { + return id; + } + + /** + * Returns map from existing unit version to list of nodes consistent ids where unit deployed. + * + * @return Map from existing unit version to list of nodes consistent ids where unit deployed. + */ + @JsonGetter("versionToNodes") + public Map<String, List<String>> versionToConsistentIds() { + return versionToConsistentIds; + } + + + /** + * Mapper method. + * + * @param status Unit status. + * @return Unit status DTO. + */ + public static UnitStatusDto fromUnitStatus(UnitStatus status) { + Map<String, List<String>> versionToConsistentIds = new HashMap<>(); + Set<Version> versions = status.versions(); + for (Version version : versions) { + versionToConsistentIds.put(version.render(), status.consistentIds(version)); + } + return new UnitStatusDto(status.id(), versionToConsistentIds); + } + +} diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/HttpCode.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/HttpCode.java index 7eed027e89..18771d74c5 100644 --- a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/HttpCode.java +++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/HttpCode.java @@ -26,6 +26,8 @@ public enum HttpCode { UNAUTHORIZED(401, "Unauthorized"), FORBIDDEN(403, "Forbidden"), NOT_FOUND(404, "Not Found"), + // May be used in case of "Already exists" problem. + CONFLICT(409, "Conflict"), INTERNAL_ERROR(500, "Internal Server Error"); private final int code; diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/MediaType.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/MediaType.java index 543ffa0b78..213d3ac8b9 100644 --- a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/MediaType.java +++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/MediaType.java @@ -37,4 +37,8 @@ public final class MediaType { * text/plain media type. */ public static final String TEXT_PLAIN = "text/plain"; + + public static final String OCTET_STREAM = "application/octet-stream"; + + public static final String FORM_DATA = "multipart/form-data"; } diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/ClusterNotInitializedException.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/ClusterNotInitializedException.java index a8547f793d..d9e349ec58 100644 --- a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/ClusterNotInitializedException.java +++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/ClusterNotInitializedException.java @@ -21,7 +21,4 @@ package org.apache.ignite.internal.rest.exception; * Exception that is thrown when the cluster is not initialized. */ public class ClusterNotInitializedException extends RuntimeException { - public ClusterNotInitializedException() { - super(); - } } diff --git a/modules/rest/build.gradle b/modules/rest/build.gradle index d2ab0ff9dd..0d4cf9c255 100644 --- a/modules/rest/build.gradle +++ b/modules/rest/build.gradle @@ -35,6 +35,7 @@ dependencies { implementation project(':ignite-network') implementation project(':ignite-cluster-management') implementation project(':ignite-metrics') + implementation project(':ignite-code-deployment') implementation project(':ignite-security') implementation libs.jetbrains.annotations implementation libs.micronaut.inject @@ -64,15 +65,17 @@ dependencies { integrationTestAnnotationProcessor libs.micronaut.inject.annotation.processor integrationTestAnnotationProcessor testFixtures(project(':ignite-core')) - integrationTestAnnotationProcessor testFixtures(project(':ignite-cluster-management')) integrationTestImplementation project(':ignite-rest-api') integrationTestImplementation project(':ignite-network') integrationTestImplementation project(':ignite-api') integrationTestImplementation project(':ignite-security') + integrationTestImplementation project(':ignite-code-deployment') + integrationTestImplementation project(':ignite-runner') integrationTestImplementation testFixtures(project(':ignite-core')) integrationTestImplementation testFixtures(project(':ignite-cluster-management')) integrationTestImplementation testFixtures(project(':ignite-configuration')) + integrationTestImplementation testFixtures(project(":ignite-api")) integrationTestImplementation libs.micronaut.junit5 integrationTestImplementation libs.micronaut.test integrationTestImplementation libs.micronaut.http.client diff --git a/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementControllerTest.java b/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementControllerTest.java new file mode 100644 index 0000000000..b87e140022 --- /dev/null +++ b/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementControllerTest.java @@ -0,0 +1,270 @@ +/* + * 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.rest.deployment; + +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.WRITE; +import static org.apache.ignite.internal.rest.constants.HttpCode.BAD_REQUEST; +import static org.apache.ignite.internal.rest.constants.HttpCode.CONFLICT; +import static org.apache.ignite.internal.rest.constants.HttpCode.NOT_FOUND; +import static org.apache.ignite.internal.rest.constants.HttpCode.OK; +import static org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.client.multipart.MultipartBody; +import io.micronaut.http.client.multipart.MultipartBody.Builder; +import jakarta.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.apache.ignite.deployment.version.Version; +import org.apache.ignite.internal.rest.api.deployment.UnitStatusDto; +import org.apache.ignite.internal.testframework.IntegrationTestBase; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +/** + * Integration test for REST controller {@link DeploymentManagementController}. + */ +public class DeploymentManagementControllerTest extends IntegrationTestBase { + private static Path dummyFile; + + private static final long SIZE_IN_BYTES = 1024L; + + @Inject + @Client(NODE_URL + "/management/v1/deployment") + HttpClient client; + + @BeforeEach + public void setup(TestInfo testInfo) throws IOException { + startNodes(testInfo); + String metaStorageNodeName = testNodeName(testInfo, 0); + initializeCluster(metaStorageNodeName); + + dummyFile = WORK_DIR.resolve("dummy.txt"); + + if (!Files.exists(dummyFile)) { + try (SeekableByteChannel channel = Files.newByteChannel(dummyFile, WRITE, CREATE)) { + channel.position(SIZE_IN_BYTES - 4); + + ByteBuffer buf = ByteBuffer.allocate(4).putInt(2); + buf.rewind(); + channel.write(buf); + } + } + } + + @AfterEach + public void cleanup(TestInfo testInfo) throws Exception { + stopNodes(testInfo); + } + + @Test + public void testDeploySuccessful() { + String id = "testId"; + String version = "1.1.1"; + HttpResponse<Object> response = deploy(id, version); + + assertThat(response.code(), is(OK.code())); + + MutableHttpRequest<Object> get = HttpRequest.GET("units"); + UnitStatusDto status = client.toBlocking().retrieve(get, UnitStatusDto.class); + + assertThat(status.id(), is(id)); + assertThat(status.versionToConsistentIds().keySet(), equalTo(Set.of(version))); + assertThat(status.versionToConsistentIds().get(version), hasItem(CLUSTER_NODE_NAMES.get(0))); + } + + @Test + public void testDeployFailedWithoutId() { + HttpClientResponseException e = assertThrows( + HttpClientResponseException.class, + () -> deploy(null, "1.1.1")); + assertThat(e.getResponse().code(), is(BAD_REQUEST.code())); + } + + @Test + public void testDeployFailedWithoutContent() { + String id = "unitId"; + String version = "1.1.1"; + HttpClientResponseException e = assertThrows( + HttpClientResponseException.class, + () -> deploy(id, version, null)); + assertThat(e.getResponse().code(), is(BAD_REQUEST.code())); + } + + @Test + public void testDeploySuccessfulWithoutVersion() { + String id = "testId"; + HttpResponse<Object> response = deploy(id); + + assertThat(response.code(), is(OK.code())); + + MutableHttpRequest<Object> get = HttpRequest.GET("units"); + UnitStatusDto status = client.toBlocking().retrieve(get, UnitStatusDto.class); + + String version = Version.LATEST.render(); + assertThat(status.id(), is(id)); + assertThat(status.versionToConsistentIds().keySet(), equalTo(Set.of(version))); + assertThat(status.versionToConsistentIds().get(version), hasItem(CLUSTER_NODE_NAMES.get(0))); + } + + @Test + public void testDeployExisted() { + String id = "testId"; + String version = "1.1.1"; + HttpResponse<Object> response = deploy(id, version); + + assertThat(response.code(), is(OK.code())); + + HttpClientResponseException e = assertThrows( + HttpClientResponseException.class, + () -> deploy(id, version)); + assertThat(e.getResponse().code(), is(CONFLICT.code())); + } + + @Test + public void testDeployUndeploy() { + String id = "testId"; + String version = "1.1.1"; + + HttpResponse<Object> response = deploy(id, version); + + assertThat(response.code(), is(OK.code())); + + response = undeploy(id, version); + assertThat(response.code(), is(OK.code())); + } + + @Test + public void testUndeployFailed() { + HttpClientResponseException e = assertThrows( + HttpClientResponseException.class, + () -> undeploy("testId", "1.1.1")); + assertThat(e.getResponse().code(), is(NOT_FOUND.code())); + } + + @Test + public void testVersionEmpty() { + String id = "nonExisted"; + assertThat(versions(id), equalTo(Collections.emptyList())); + } + + @Test + public void testDeployUndeployLatest() { + String id = "testId"; + HttpResponse<Object> response = deploy(id); + + assertThat(response.code(), is(OK.code())); + MutableHttpRequest<Object> delete = HttpRequest + .DELETE("units/" + id) + .contentType(MediaType.APPLICATION_JSON); + response = client.toBlocking().exchange(delete); + assertThat(response.code(), is(OK.code())); + } + + @Test + public void testVersionOrder() { + String id = "unitId"; + deploy(id); + deploy(id, "1.1.1"); + deploy(id, "1.1.2"); + deploy(id, "1.2.1"); + deploy(id, "2.0"); + deploy(id, "1.0.0"); + deploy(id, "1.0.1"); + + List<String> versions = versions(id); + + assertThat(versions, contains("1.0.0", "1.0.1", "1.1.1", "1.1.2", "1.2.1", "2.0.0", "latest")); + } + + private HttpResponse<Object> deploy(String id) { + return deploy(id, null); + } + + private HttpResponse<Object> deploy(String id, String version) { + return deploy(id, version, dummyFile.toFile()); + } + + private HttpResponse<Object> deploy(String id, String version, File file) { + Builder builder = MultipartBody.builder() + .addPart("unitVersion", version); + + if (id != null) { + builder.addPart("unitId", id); + } + if (file != null) { + builder.addPart("unitContent", file); + } + + MutableHttpRequest<MultipartBody> post = HttpRequest.POST("units", builder.build()) + .contentType(MediaType.MULTIPART_FORM_DATA); + return client.toBlocking().exchange(post); + } + + private HttpResponse<Object> undeploy(String id) { + MutableHttpRequest<Object> delete = HttpRequest + .DELETE("units/" + id) + .contentType(MediaType.APPLICATION_JSON); + + return client.toBlocking().exchange(delete); + } + + private HttpResponse<Object> undeploy(String id, String version) { + MutableHttpRequest<Object> delete = HttpRequest + .DELETE("units/" + id + "/" + version) + .contentType(MediaType.APPLICATION_JSON); + + return client.toBlocking().exchange(delete); + } + + private List<String> versions(String id) { + MutableHttpRequest<Object> versions = HttpRequest + .GET("units/" + id + "/versions") + .contentType(MediaType.APPLICATION_JSON); + + return client.toBlocking().retrieve(versions, List.class); + + } + + private UnitStatusDto status(String id) { + MutableHttpRequest<Object> get = HttpRequest.GET("units/" + id + "/status"); + return client.toBlocking().retrieve(get, UnitStatusDto.class); + } +} diff --git a/modules/api/src/main/java/org/apache/ignite/deployment/version/VersionParseException.java b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/CodeDeploymentRestFactory.java similarity index 54% copy from modules/api/src/main/java/org/apache/ignite/deployment/version/VersionParseException.java copy to modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/CodeDeploymentRestFactory.java index 717035a9c0..a75c167f2c 100644 --- a/modules/api/src/main/java/org/apache/ignite/deployment/version/VersionParseException.java +++ b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/CodeDeploymentRestFactory.java @@ -15,34 +15,28 @@ * limitations under the License. */ -package org.apache.ignite.deployment.version; +package org.apache.ignite.internal.rest.deployment; + +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Factory; +import jakarta.inject.Singleton; +import org.apache.ignite.deployment.IgniteDeployment; +import org.apache.ignite.internal.rest.RestFactory; /** - * Throws when {@link Version} of deployment unit not parsable. + * Factory of {@link DeploymentManagementController}. */ -public class VersionParseException extends RuntimeException { - /** - * Constructor. - */ - public VersionParseException() { - - } +@Factory +public class CodeDeploymentRestFactory implements RestFactory { + private final IgniteDeployment igniteDeployment; - /** - * Constructor. - * - * @param cause Cause error. - */ - public VersionParseException(Throwable cause) { - super(cause); + public CodeDeploymentRestFactory(IgniteDeployment igniteDeployment) { + this.igniteDeployment = igniteDeployment; } - /** - * Constructor. - * - * @param message Error message. - */ - public VersionParseException(String message) { - super(message); + @Bean + @Singleton + public IgniteDeployment deployment() { + return igniteDeployment; } } diff --git a/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementController.java b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementController.java new file mode 100644 index 0000000000..cf8fa908ff --- /dev/null +++ b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementController.java @@ -0,0 +1,99 @@ +/* + * 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.rest.deployment; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.multipart.CompletedFileUpload; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import org.apache.ignite.deployment.DeploymentUnit; +import org.apache.ignite.deployment.IgniteDeployment; +import org.apache.ignite.deployment.version.Version; +import org.apache.ignite.internal.rest.api.deployment.DeploymentCodeApi; +import org.apache.ignite.internal.rest.api.deployment.UnitStatusDto; + +/** + * Implementation of {@link DeploymentCodeApi}. + */ +@Controller("/management/v1/deployment") +public class DeploymentManagementController implements DeploymentCodeApi { + private final IgniteDeployment deployment; + + public DeploymentManagementController(IgniteDeployment deployment) { + this.deployment = deployment; + } + + @Override + public CompletableFuture<Boolean> deploy(String unitId, String unitVersion, CompletedFileUpload unitContent) { + try { + DeploymentUnit deploymentUnit = toDeploymentUnit(unitContent); + if (unitVersion == null || unitVersion.isBlank()) { + return deployment.deployAsync(unitId, deploymentUnit); + } + return deployment.deployAsync(unitId, Version.parseVersion(unitVersion), deploymentUnit); + } catch (IOException e) { + return CompletableFuture.failedFuture(e); + } + } + + @Override + public CompletableFuture<Void> undeploy(String unitId, String unitVersion) { + return deployment.undeployAsync(unitId, Version.parseVersion(unitVersion)); + } + + @Override + public CompletableFuture<Void> undeploy(String unitId) { + return deployment.undeployAsync(unitId); + } + + @Override + public CompletableFuture<Collection<UnitStatusDto>> units() { + return deployment.unitsAsync().thenApply(statuses -> statuses.stream().map(UnitStatusDto::fromUnitStatus) + .collect(Collectors.toList())); + } + + @Override + public CompletableFuture<Collection<String>> versions(String unitId) { + return deployment.versionsAsync(unitId) + .thenApply(versions -> versions.stream().map(Version::render).collect(Collectors.toList())); + } + + @Override + public CompletableFuture<UnitStatusDto> status(String unitId) { + return deployment.statusAsync(unitId).thenApply(UnitStatusDto::fromUnitStatus); + } + + private static DeploymentUnit toDeploymentUnit(CompletedFileUpload unitContent) throws IOException { + String fileName = unitContent.getFilename(); + InputStream is = unitContent.getInputStream(); + return new DeploymentUnit() { + @Override + public String name() { + return fileName; + } + + @Override + public InputStream content() { + return is; + } + }; + } +} diff --git a/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/exception/handler/DeploymentUnitAlreadyExistExceptionHandler.java b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/exception/handler/DeploymentUnitAlreadyExistExceptionHandler.java new file mode 100644 index 0000000000..89ce644a83 --- /dev/null +++ b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/exception/handler/DeploymentUnitAlreadyExistExceptionHandler.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.ignite.internal.rest.deployment.exception.handler; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import jakarta.inject.Singleton; +import org.apache.ignite.internal.deployunit.exception.DeploymentUnitAlreadyExistsException; +import org.apache.ignite.internal.rest.api.Problem; +import org.apache.ignite.internal.rest.constants.HttpCode; +import org.apache.ignite.internal.rest.problem.HttpProblemResponse; + +/** + * REST exception handler for {@link DeploymentUnitAlreadyExistsException}. + */ +@Singleton +@Requires(classes = {DeploymentUnitAlreadyExistsException.class, ExceptionHandler.class}) +public class DeploymentUnitAlreadyExistExceptionHandler + implements ExceptionHandler<DeploymentUnitAlreadyExistsException, HttpResponse<? extends Problem>> { + @Override + public HttpResponse<? extends Problem> handle(HttpRequest request, DeploymentUnitAlreadyExistsException exception) { + return HttpProblemResponse.from( + Problem.fromHttpCode(HttpCode.CONFLICT) + .detail(exception.getMessage()).build() + ); + } +} diff --git a/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/exception/handler/DeploymentUnitNotFoundExceptionHandler.java b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/exception/handler/DeploymentUnitNotFoundExceptionHandler.java new file mode 100644 index 0000000000..505dbadd20 --- /dev/null +++ b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/exception/handler/DeploymentUnitNotFoundExceptionHandler.java @@ -0,0 +1,45 @@ +/* + * 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.rest.deployment.exception.handler; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import jakarta.inject.Singleton; +import org.apache.ignite.internal.deployunit.exception.DeploymentUnitNotFoundException; +import org.apache.ignite.internal.rest.api.Problem; +import org.apache.ignite.internal.rest.constants.HttpCode; +import org.apache.ignite.internal.rest.problem.HttpProblemResponse; + +/** + * REST exception handler for {@link DeploymentUnitNotFoundException}. + */ +@Singleton +@Requires(classes = {DeploymentUnitNotFoundException.class, ExceptionHandler.class}) +public class DeploymentUnitNotFoundExceptionHandler implements + ExceptionHandler<DeploymentUnitNotFoundException, HttpResponse<? extends Problem>> { + + @Override + public HttpResponse<? extends Problem> handle(HttpRequest request, DeploymentUnitNotFoundException exception) { + return HttpProblemResponse.from( + Problem.fromHttpCode(HttpCode.NOT_FOUND) + .detail(exception.getMessage()).build() + ); + } +} diff --git a/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/exception/handler/VersionParseExceptionHandler.java b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/exception/handler/VersionParseExceptionHandler.java new file mode 100644 index 0000000000..752cfe78a8 --- /dev/null +++ b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/exception/handler/VersionParseExceptionHandler.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.ignite.internal.rest.deployment.exception.handler; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import jakarta.inject.Singleton; +import org.apache.ignite.deployment.version.VersionParseException; +import org.apache.ignite.internal.rest.api.Problem; +import org.apache.ignite.internal.rest.constants.HttpCode; +import org.apache.ignite.internal.rest.problem.HttpProblemResponse; + +/** + * REST exception handler for {@link VersionParseException}. + */ +@Singleton +@Requires(classes = {VersionParseException.class, ExceptionHandler.class}) +public class VersionParseExceptionHandler implements ExceptionHandler<VersionParseException, HttpResponse<? extends Problem>> { + @Override + public HttpResponse<? extends Problem> handle(HttpRequest request, VersionParseException exception) { + return HttpProblemResponse.from( + Problem.fromHttpCode(HttpCode.BAD_REQUEST) + .detail("Invalid version format of provided version: " + exception.getRawVersion()) + .build() + ); + } +} diff --git a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java index d06b454a16..ff05278575 100644 --- a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java +++ b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java @@ -103,6 +103,7 @@ import org.apache.ignite.internal.rest.authentication.AuthProviderFactory; import org.apache.ignite.internal.rest.cluster.ClusterManagementRestFactory; import org.apache.ignite.internal.rest.configuration.PresentationsFactory; import org.apache.ignite.internal.rest.configuration.RestConfiguration; +import org.apache.ignite.internal.rest.deployment.CodeDeploymentRestFactory; import org.apache.ignite.internal.rest.metrics.MetricRestFactory; import org.apache.ignite.internal.rest.node.NodeManagementRestFactory; import org.apache.ignite.internal.schema.SchemaManager; @@ -414,8 +415,6 @@ public class IgniteImpl implements Ignite { DistributionZonesConfiguration zonesConfiguration = clusterConfigRegistry .getConfiguration(DistributionZonesConfiguration.KEY); - restComponent = createRestComponent(name); - restAddressReporter = new RestAddressReporter(workDir); baselineMgr = new BaselineManager( @@ -518,6 +517,8 @@ public class IgniteImpl implements Ignite { workDir, nodeConfigRegistry.getConfiguration(DeploymentConfiguration.KEY), cmgMgr); + + restComponent = createRestComponent(name); } private RestComponent createRestComponent(String name) { @@ -529,12 +530,14 @@ public class IgniteImpl implements Ignite { RestFactory nodeManagementRestFactory = new NodeManagementRestFactory(lifecycleManager, () -> name); RestFactory nodeMetricRestFactory = new MetricRestFactory(metricManager); AuthProviderFactory authProviderFactory = new AuthProviderFactory(authConfiguration); + RestFactory deploymentCodeRestFactory = new CodeDeploymentRestFactory(deploymentManager); RestConfiguration restConfiguration = nodeCfgMgr.configurationRegistry().getConfiguration(RestConfiguration.KEY); return new RestComponent( List.of(presentationsFactory, clusterManagementRestFactory, nodeManagementRestFactory, nodeMetricRestFactory, + deploymentCodeRestFactory, authProviderFactory), restConfiguration );