diff --git a/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java b/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java index 3bc6b94e..09e7a5bf 100644 --- a/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java +++ b/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java @@ -108,6 +108,21 @@ public Future undeployApplication() { return Future.succeededFuture(); } + public Future redeployApplication() { + context.getRequest() + .body() + .compose(body -> { + String url = ProxyUtil.convertToObject(body, ResourceLink.class).url(); + ResourceDescriptor resource = decodeUrl(url); + checkAccess(resource); + return vertx.executeBlocking(() -> applicationService.redeployApplication(context, resource), false); + }) + .onSuccess(application -> context.respond(HttpStatus.OK, application)) + .onFailure(this::respondError); + + return Future.succeededFuture(); + } + public Future getApplicationLogs() { context.getRequest() .body() diff --git a/server/src/main/java/com/epam/aidial/core/server/controller/ControllerSelector.java b/server/src/main/java/com/epam/aidial/core/server/controller/ControllerSelector.java index 194db8c2..3268ccb7 100644 --- a/server/src/main/java/com/epam/aidial/core/server/controller/ControllerSelector.java +++ b/server/src/main/java/com/epam/aidial/core/server/controller/ControllerSelector.java @@ -43,7 +43,7 @@ public class ControllerSelector { private static final Pattern PATTERN_APPLICATION = Pattern.compile("^/+openai/applications/(?.+?)$"); private static final Pattern PATTERN_APPLICATIONS = Pattern.compile("^/+openai/applications$"); - private static final Pattern APPLICATIONS = Pattern.compile("^/v1/ops/application/(deploy|undeploy|logs)$"); + private static final Pattern APPLICATIONS = Pattern.compile("^/v1/ops/application/(deploy|undeploy|logs|redeploy)$"); private static final Pattern PATTERN_BUCKET = Pattern.compile("^/v1/bucket$"); @@ -285,6 +285,7 @@ public class ControllerSelector { case "deploy" -> controller::deployApplication; case "undeploy" -> controller::undeployApplication; case "logs" -> controller::getApplicationLogs; + case "redeploy" -> controller::redeployApplication; default -> null; }; }); diff --git a/server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java b/server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java index 2d186c00..ea93894a 100644 --- a/server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java +++ b/server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java @@ -25,6 +25,7 @@ import com.epam.aidial.core.storage.service.ResourceService; import com.epam.aidial.core.storage.util.EtagHeader; import com.epam.aidial.core.storage.util.UrlUtil; +import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; import lombok.Getter; @@ -339,6 +340,17 @@ public void copyApplication(ResourceDescriptor source, ResourceDescriptor destin } } + public Application redeployApplication(ProxyContext context, ResourceDescriptor resource) { + verifyApplication(resource); + controller.verifyActive(); + + Pair> result = undeployApplicationInternal(resource); + + result.getValue().map(ignore -> deployApplication(context, resource)) + .onFailure(error -> log.error("Application redeployment is failed due to the error", error)); + return result.getKey(); + } + public Application deployApplication(ProxyContext context, ResourceDescriptor resource) { verifyApplication(resource); controller.verifyActive(); @@ -374,6 +386,10 @@ public Application deployApplication(ProxyContext context, ResourceDescriptor re } public Application undeployApplication(ResourceDescriptor resource) { + return undeployApplicationInternal(resource).getKey(); + } + + private Pair> undeployApplicationInternal(ResourceDescriptor resource) { verifyApplication(resource); controller.verifyActive(); @@ -405,8 +421,8 @@ public Application undeployApplication(ResourceDescriptor resource) { return ProxyUtil.convertToString(application); }); - vertx.executeBlocking(() -> terminateApplication(resource, null), false); - return result.getValue(); + Future future = vertx.executeBlocking(() -> terminateApplication(resource, null), false); + return Pair.of(result.getValue(), future); } public Application.Logs getApplicationLogs(ResourceDescriptor resource) { diff --git a/server/src/test/java/com/epam/aidial/core/server/ApplicationDeploymentApiTest.java b/server/src/test/java/com/epam/aidial/core/server/ApplicationDeploymentApiTest.java index 8b041f14..36e89afe 100644 --- a/server/src/test/java/com/epam/aidial/core/server/ApplicationDeploymentApiTest.java +++ b/server/src/test/java/com/epam/aidial/core/server/ApplicationDeploymentApiTest.java @@ -221,6 +221,91 @@ void testApplicationStopped() { """); } + @Test + void testApplicationRestarted() { + testApplicationStarted(); + + webServer.map(HttpMethod.DELETE, "/v1/image/0123", 200, + """ + event: result + data: {} + """); + webServer.map(HttpMethod.DELETE, "/v1/deployment/0123", 200, + """ + event: result + data: {"deleted":true} + """); + + Response response = send(HttpMethod.POST, "/v1/ops/application/redeploy", null, """ + { + "url": "applications/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/my-app" + } + """); + verifyJsonNotExact(response, 200, """ + { + "name" : "applications/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/my-app", + "display_name" : "My App", + "display_version" : "1.0", + "icon_url" : "http://application1/icon.svg", + "description" : "My App Description", + "reference" : "@ignore", + "forward_auth_token" : false, + "features" : { }, + "defaults" : { }, + "interceptors" : [ ], + "description_keywords" : [ ], + "max_retry_attempts" : 1, + "function" : { + "id" : "0123", + "runtime": "python3.11", + "author_bucket" : "3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST", + "source_folder" : "files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/my-app/", + "target_folder" : "files/2CZ9i2bcBACFts8JbBu3MdcF8sdwTbELGXeFRV6CVDwnPEU8vWC1y8PpXyRChHQvzt/", + "status" : "UNDEPLOYING", + "mapping" : { + "chat_completion" : "/application" + }, + "env" : { + "VAR" : "VAL" + } + } + } + """); + + response = awaitApplicationStatus("/v1/applications/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/my-app", "DEPLOYED"); + verifyJsonNotExact(response, 200, """ + { + "name" : "applications/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/my-app", + "endpoint" : "http://localhost:17321/application", + "display_name" : "My App", + "display_version" : "1.0", + "icon_url" : "http://application1/icon.svg", + "description" : "My App Description", + "reference" : "@ignore", + "forward_auth_token" : false, + "features" : { }, + "defaults" : { }, + "interceptors" : [ ], + "description_keywords" : [ ], + "max_retry_attempts" : 1, + "function" : { + "id" : "0123", + "runtime": "python3.11", + "author_bucket" : "3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST", + "source_folder" : "files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/my-app/", + "target_folder" : "files/2CZ9i2bcBACFts8JbBu3MdcF8sdwTbELGXeFRV6CVDwnPEU8vWC1y8PpXyRChHQvzt/", + "status" : "DEPLOYED", + "mapping" : { + "chat_completion" : "/application" + }, + "env" : { + "VAR" : "VAL" + } + } + } + """); + } + @Test void testApplicationFailed() { testApplicationCreated();