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
         );

Reply via email to