diff --git a/kura/distrib/config/kura.build.properties b/kura/distrib/config/kura.build.properties
index baf0a621337..e0b642dea84 100644
--- a/kura/distrib/config/kura.build.properties
+++ b/kura/distrib/config/kura.build.properties
@@ -114,6 +114,7 @@ org.eclipse.kura.log.filesystem.provider.version=1.3.0-SNAPSHOT
org.eclipse.kura.rest.configuration.provider.version=1.3.0-SNAPSHOT
org.eclipse.kura.rest.inventory.provider.version=1.0.0-SNAPSHOT
org.eclipse.kura.rest.command.provider.version=1.0.0-SNAPSHOT
+org.eclipse.kura.rest.packages.provider.version=1.0.0-SNAPSHOT
org.eclipse.kura.rest.position.provider.version=1.0.0-SNAPSHOT
org.eclipse.kura.rest.security.provider.version=1.0.0-SNAPSHOT
org.eclipse.kura.rest.service.listing.provider.version=1.0.0-SNAPSHOT
diff --git a/kura/distrib/pom.xml b/kura/distrib/pom.xml
index 32bc66932bf..cb1a0a61e16 100644
--- a/kura/distrib/pom.xml
+++ b/kura/distrib/pom.xml
@@ -608,6 +608,11 @@
org.eclipse.kura.rest.command.provider
${org.eclipse.kura.rest.command.provider.version}
+
+ org.eclipse.kura
+ org.eclipse.kura.rest.packages.provider
+ ${org.eclipse.kura.rest.packages.provider.version}
+
org.eclipse.kura
org.eclipse.kura.rest.position.provider
@@ -820,6 +825,7 @@
+
@@ -2535,6 +2541,7 @@
+
diff --git a/kura/distrib/src/main/ant/build_equinox_distrib.xml b/kura/distrib/src/main/ant/build_equinox_distrib.xml
index 9fc40a71132..4a0980c14e5 100644
--- a/kura/distrib/src/main/ant/build_equinox_distrib.xml
+++ b/kura/distrib/src/main/ant/build_equinox_distrib.xml
@@ -1315,6 +1315,8 @@ fi]]>
value=", reference:file:${kura.install.dir}/${kura.symlink}/${plugins.folder}/org.eclipse.kura.rest.inventory.provider_${org.eclipse.kura.rest.inventory.provider.version}.jar@4:start" />
+
+
diff --git a/kura/org.eclipse.kura.rest.packages.provider/META-INF/MANIFEST.MF b/kura/org.eclipse.kura.rest.packages.provider/META-INF/MANIFEST.MF
new file mode 100644
index 00000000000..30f61695a70
--- /dev/null
+++ b/kura/org.eclipse.kura.rest.packages.provider/META-INF/MANIFEST.MF
@@ -0,0 +1,22 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Kura Deployment Packages Rest Provider
+Bundle-SymbolicName: org.eclipse.kura.rest.packages.provider;singleton:=true
+Bundle-Version: 1.0.0.qualifier
+Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"
+Bundle-ClassPath: .
+Bundle-ActivationPolicy: lazy
+Service-Component: OSGI-INF/*.xml
+Import-Package: com.google.gson;version="2.7.0",
+ javax.annotation.security;version="1.2.0",
+ javax.ws.rs;version="2.0.1",
+ javax.ws.rs.core;version="2.0.1",
+ javax.ws.rs.ext;version="2.0.1",
+ org.eclipse.kura;version="[1.3,2.0)",
+ org.eclipse.kura.deployment.agent;version="[1.0,2.0)",
+ org.osgi.framework;version="1.8.0",
+ org.osgi.service.component;version="1.3.0",
+ org.osgi.service.deploymentadmin;version="1.0.0",
+ org.osgi.service.useradmin;version="1.1.0";resolution:=optional,
+ org.slf4j;version="1.7.21"
+Export-Package: org.eclipse.kura.rest.deployment.agent.api;version="1.0.0";x-internal:=true
diff --git a/kura/org.eclipse.kura.rest.packages.provider/OSGI-INF/deployment_rest_service.xml b/kura/org.eclipse.kura.rest.packages.provider/OSGI-INF/deployment_rest_service.xml
new file mode 100644
index 00000000000..ff7168fc457
--- /dev/null
+++ b/kura/org.eclipse.kura.rest.packages.provider/OSGI-INF/deployment_rest_service.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kura/org.eclipse.kura.rest.packages.provider/build.properties b/kura/org.eclipse.kura.rest.packages.provider/build.properties
new file mode 100644
index 00000000000..6de2df69d5a
--- /dev/null
+++ b/kura/org.eclipse.kura.rest.packages.provider/build.properties
@@ -0,0 +1,17 @@
+#
+# Copyright (c) 2023 Eurotech and/or its affiliates and others
+#
+# This program and the accompanying materials are made
+# available under the terms of the Eclipse Public License 2.0
+# which is available at https://www.eclipse.org/legal/epl-2.0/
+#
+# SPDX-License-Identifier: EPL-2.0
+#
+# Contributors:
+# Eurotech
+#
+output.. = target/classes
+bin.includes = .,\
+ META-INF/,\
+ OSGI-INF/
+source.. = src/main/java/
diff --git a/kura/org.eclipse.kura.rest.packages.provider/pom.xml b/kura/org.eclipse.kura.rest.packages.provider/pom.xml
new file mode 100644
index 00000000000..a367a0c810a
--- /dev/null
+++ b/kura/org.eclipse.kura.rest.packages.provider/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+ 4.0.0
+
+
+ org.eclipse.kura
+ kura
+ 5.4.0-SNAPSHOT
+
+
+
+ ${project.basedir}/..
+
+ ${project.basedir}/../test/*/target/site/jacoco-aggregate/jacoco.xml
+
+
+ org.eclipse.kura.rest.packages.provider
+ 1.0.0-SNAPSHOT
+ eclipse-plugin
+
diff --git a/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/internal/rest/deployment/agent/DeploymentPackageInfo.java b/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/internal/rest/deployment/agent/DeploymentPackageInfo.java
new file mode 100644
index 00000000000..ef69aa7b255
--- /dev/null
+++ b/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/internal/rest/deployment/agent/DeploymentPackageInfo.java
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright (c) 2023 Eurotech and/or its affiliates and others
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Eurotech
+ *******************************************************************************/
+package org.eclipse.kura.internal.rest.deployment.agent;
+
+public class DeploymentPackageInfo {
+
+ private final String name;
+ private final String version;
+
+ public DeploymentPackageInfo(String name, String version) {
+ this.name = name;
+ this.version = version;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getVersion() {
+ return this.version;
+ }
+
+}
diff --git a/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/internal/rest/deployment/agent/DeploymentRestService.java b/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/internal/rest/deployment/agent/DeploymentRestService.java
new file mode 100644
index 00000000000..62ebaae9a44
--- /dev/null
+++ b/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/internal/rest/deployment/agent/DeploymentRestService.java
@@ -0,0 +1,143 @@
+/*******************************************************************************
+ * Copyright (c) 2023 Eurotech and/or its affiliates and others
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Eurotech
+ *******************************************************************************/
+package org.eclipse.kura.internal.rest.deployment.agent;
+
+import static org.eclipse.kura.rest.deployment.agent.api.Validable.validate;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.eclipse.kura.deployment.agent.DeploymentAgentService;
+import org.eclipse.kura.rest.deployment.agent.api.DeploymentRequestStatus;
+import org.eclipse.kura.rest.deployment.agent.api.InstallRequest;
+import org.osgi.service.deploymentadmin.DeploymentAdmin;
+import org.osgi.service.deploymentadmin.DeploymentPackage;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdmin;
+
+@Path("/deploy/v2")
+public class DeploymentRestService {
+
+ private static final String KURA_PERMISSION_REST_DEPLOY_ROLE = "kura.permission.rest.deploy";
+ private static final String ERROR_INSTALLING_PACKAGE = "Error installing deployment package: ";
+ private static final String ERROR_UNINSTALLING_PACKAGE = "Error uninstalling deployment package: ";
+ private static final String BAD_REQUEST_MESSAGE = "Bad request";
+
+ private DeploymentAdmin deploymentAdmin;
+ private DeploymentAgentService deploymentAgentService;
+ private UserAdmin userAdmin;
+
+ public void setUserAdmin(UserAdmin userAdmin) {
+ this.userAdmin = userAdmin;
+ this.userAdmin.createRole(KURA_PERMISSION_REST_DEPLOY_ROLE, Role.GROUP);
+ }
+
+ public void setDeploymentAdmin(DeploymentAdmin deploymentAdmin) {
+ this.deploymentAdmin = deploymentAdmin;
+ }
+
+ public void setDeploymentAgentService(DeploymentAgentService deploymentAgentService) {
+ this.deploymentAgentService = deploymentAgentService;
+ }
+
+ /**
+ * GET method.
+ *
+ * Provides the list of all the deployment packages installed and tracked by the framework.
+ *
+ * @return a list of {@link DeploymentPackageInfo}
+ */
+ @GET
+ @RolesAllowed("deploy")
+ @Produces(MediaType.APPLICATION_JSON)
+ public List listDeploymentPackages() {
+
+ List deploymentPackageInfos = new ArrayList<>();
+ List deploymentPackages = Arrays.asList(this.deploymentAdmin.listDeploymentPackages());
+
+ deploymentPackages.forEach(
+ dp -> deploymentPackageInfos.add(new DeploymentPackageInfo(dp.getName(), dp.getVersion().toString())));
+
+ return deploymentPackageInfos;
+ }
+
+ /**
+ * POST method.
+ *
+ * Installs the deployment package specified in the {@link InstallRequest}. If the request was already issued for
+ * the same {@link InstallRequest}, it returns the status of the installation process.
+ *
+ * @param installRequest
+ * @return a {@link DeploymentRequestStatus} object that represents the status of the installation request
+ */
+ @POST
+ @RolesAllowed("deploy")
+ @Path("/_install")
+ @Produces(MediaType.APPLICATION_JSON)
+ public DeploymentRequestStatus installDeploymentPackage(InstallRequest installRequest) {
+ validate(installRequest, BAD_REQUEST_MESSAGE);
+ String url = installRequest.getUrl();
+
+ if (this.deploymentAgentService.isInstallingDeploymentPackage(url)) {
+ return DeploymentRequestStatus.INSTALLING;
+ }
+
+ try {
+ this.deploymentAgentService.installDeploymentPackageAsync(url);
+ } catch (Exception e) {
+ throw new WebApplicationException(Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .type(MediaType.TEXT_PLAIN).entity(ERROR_INSTALLING_PACKAGE + url).build());
+ }
+
+ return DeploymentRequestStatus.REQUEST_RECEIVED;
+ }
+
+ /**
+ * DELETE method.
+ *
+ * Uninstalls the deployment package identified by the specified name. If the request was already issued, it reports
+ * the status of the uninstallation operation.
+ *
+ * @param name
+ * @return a {@link DeploymentRequestStatus} object that represents the status of the uninstallation request
+ */
+ @DELETE
+ @RolesAllowed("deploy")
+ @Path("/{name}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public DeploymentRequestStatus uninstallDeploymentPackage(@PathParam("name") String name) {
+ if (this.deploymentAgentService.isUninstallingDeploymentPackage(name)) {
+ return DeploymentRequestStatus.UNINSTALLING;
+ }
+ try {
+ this.deploymentAgentService.uninstallDeploymentPackageAsync(name);
+ } catch (Exception e) {
+ throw new WebApplicationException(Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .type(MediaType.TEXT_PLAIN).entity(ERROR_UNINSTALLING_PACKAGE + name).build());
+ }
+
+ return DeploymentRequestStatus.REQUEST_RECEIVED;
+ }
+}
diff --git a/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/rest/deployment/agent/api/DeploymentRequestStatus.java b/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/rest/deployment/agent/api/DeploymentRequestStatus.java
new file mode 100644
index 00000000000..32335482d96
--- /dev/null
+++ b/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/rest/deployment/agent/api/DeploymentRequestStatus.java
@@ -0,0 +1,23 @@
+/*******************************************************************************
+ * Copyright (c) 2023 Eurotech and/or its affiliates and others
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Eurotech
+ *******************************************************************************/
+package org.eclipse.kura.rest.deployment.agent.api;
+
+/**
+ * Enumeration representing the status of the deployment requests received via REST.
+ *
+ */
+public enum DeploymentRequestStatus {
+ REQUEST_RECEIVED,
+ INSTALLING,
+ UNINSTALLING;
+}
diff --git a/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/rest/deployment/agent/api/InstallRequest.java b/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/rest/deployment/agent/api/InstallRequest.java
new file mode 100644
index 00000000000..0d6c544ab35
--- /dev/null
+++ b/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/rest/deployment/agent/api/InstallRequest.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2023 Eurotech and/or its affiliates and others
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Eurotech
+ *******************************************************************************/
+package org.eclipse.kura.rest.deployment.agent.api;
+
+public class InstallRequest implements Validable {
+
+ private final String url;
+
+ public InstallRequest(String url) {
+ this.url = url;
+ }
+
+ public String getUrl() {
+ return this.url;
+ }
+
+ @Override
+ public boolean isValid() {
+ return this.url != null;
+ }
+
+}
diff --git a/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/rest/deployment/agent/api/Validable.java b/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/rest/deployment/agent/api/Validable.java
new file mode 100644
index 00000000000..f96afa79b85
--- /dev/null
+++ b/kura/org.eclipse.kura.rest.packages.provider/src/main/java/org/eclipse/kura/rest/deployment/agent/api/Validable.java
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * Copyright (c) 2023 Eurotech and/or its affiliates and others
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Eurotech
+ *******************************************************************************/
+package org.eclipse.kura.rest.deployment.agent.api;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+public interface Validable {
+
+ public boolean isValid();
+
+ public static boolean isValid(Validable validable) {
+ if (validable == null) {
+ return false;
+ }
+ return validable.isValid();
+ }
+
+ public static void validate(Validable validable, String exceptionMessage) {
+ if (!isValid(validable)) {
+ throw new WebApplicationException(
+ Response.status(Status.BAD_REQUEST).entity(exceptionMessage).type(MediaType.TEXT_PLAIN).build());
+ }
+ }
+}
diff --git a/kura/pom.xml b/kura/pom.xml
index 753128aaf96..5b084074f5b 100644
--- a/kura/pom.xml
+++ b/kura/pom.xml
@@ -110,6 +110,7 @@
org.eclipse.kura.rest.configuration.provider
org.eclipse.kura.rest.inventory.provider
org.eclipse.kura.rest.command.provider
+ org.eclipse.kura.rest.packages.provider
org.eclipse.kura.rest.position.provider
org.eclipse.kura.rest.security.provider
org.eclipse.kura.rest.service.listing.provider
diff --git a/kura/test/org.eclipse.kura.rest.packages.provider.test/META-INF/MANIFEST.MF b/kura/test/org.eclipse.kura.rest.packages.provider.test/META-INF/MANIFEST.MF
new file mode 100644
index 00000000000..21ca436dc92
--- /dev/null
+++ b/kura/test/org.eclipse.kura.rest.packages.provider.test/META-INF/MANIFEST.MF
@@ -0,0 +1,34 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: org.eclipse.kura.rest.packages.provider.test
+Bundle-SymbolicName: org.eclipse.kura.rest.packages.provider.test
+Bundle-Version: 5.4.0.qualifier
+Bundle-Vendor: Eclipse Kura
+Bundle-License: Eclipse Public License v2.0
+Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"
+Bundle-ActivationPolicy: lazy
+Fragment-Host: org.eclipse.kura.rest.packages.provider
+Import-Package: com.eclipsesource.json;version="0.9.5",
+ org.apache.commons.io;version="2.4.0",
+ org.eclipse.kura;version="1.6.0",
+ org.eclipse.kura.core.testutil.json;version="1.0.0",
+ org.eclipse.kura.core.testutil.requesthandler;version="1.0.0",
+ org.eclipse.kura.core.util;version="1.3.0",
+ org.eclipse.kura.data;version="1.1.2",
+ org.eclipse.kura.data.transport.listener;version="1.0.1",
+ org.eclipse.kura.marshalling;version="1.0.0",
+ org.eclipse.kura.message;version="1.4.0",
+ org.junit;version="[4.12.0,5.0.0)",
+ org.junit.runner;version="[4.12.0,5.0.0)",
+ org.junit.runners;version="[4.12.0,5.0.0)",
+ org.mockito;version="[4.0.0,5.0.0)",
+ org.mockito.invocation;version="[4.0.0,5.0.0)",
+ org.mockito.stubbing;version="[4.0.0,5.0.0)",
+ org.osgi.service.deploymentadmin;version="1.0.0",
+ org.osgi.framework;version="1.10.0",
+ org.osgi.service.cm;version="1.6.0",
+ org.osgi.util.tracker;version="1.5.2",
+ org.slf4j;version="1.7.25"
+Require-Bundle: org.eclipse.kura.http.server.manager;bundle-version="1.1.0",
+ org.eclipse.kura.broker.artemis.core;bundle-version="1.2.0",
+ org.eclipse.kura.broker.artemis.simple.mqtt;bundle-version="1.1.0"
diff --git a/kura/test/org.eclipse.kura.rest.packages.provider.test/build.properties b/kura/test/org.eclipse.kura.rest.packages.provider.test/build.properties
new file mode 100644
index 00000000000..60a73b4fd62
--- /dev/null
+++ b/kura/test/org.eclipse.kura.rest.packages.provider.test/build.properties
@@ -0,0 +1,15 @@
+#
+# Copyright (c) 2023 Eurotech and/or its affiliates and others
+#
+# This program and the accompanying materials are made
+# available under the terms of the Eclipse Public License 2.0
+# which is available at https://www.eclipse.org/legal/epl-2.0/
+#
+# SPDX-License-Identifier: EPL-2.0
+#
+# Contributors:
+# Eurotech
+#
+source.. = src/main/java/
+bin.includes = META-INF/,\
+ .
diff --git a/kura/test/org.eclipse.kura.rest.packages.provider.test/pom.xml b/kura/test/org.eclipse.kura.rest.packages.provider.test/pom.xml
new file mode 100644
index 00000000000..df411a37852
--- /dev/null
+++ b/kura/test/org.eclipse.kura.rest.packages.provider.test/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+ 4.0.0
+
+
+ org.eclipse.kura
+ test
+ 5.4.0-SNAPSHOT
+
+
+ org.eclipse.kura.rest.packages.provider.test
+ eclipse-test-plugin
+
+
+ ${project.basedir}/../..
+
+ ${project.build.directory}/site/jacoco-aggregate/jacoco.xml
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ compiletests
+ test-compile
+
+ testCompile
+
+
+
+
+
+ org.eclipse.tycho
+ tycho-surefire-plugin
+
+ classes
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ org.eclipse.tycho
+ target-platform-configuration
+
+
+
+
diff --git a/kura/test/org.eclipse.kura.rest.packages.provider.test/src/main/java/org/eclipse/kura/rest/packages/provider/test/PackagesRestServiceTest.java b/kura/test/org.eclipse.kura.rest.packages.provider.test/src/main/java/org/eclipse/kura/rest/packages/provider/test/PackagesRestServiceTest.java
new file mode 100644
index 00000000000..667561e71db
--- /dev/null
+++ b/kura/test/org.eclipse.kura.rest.packages.provider.test/src/main/java/org/eclipse/kura/rest/packages/provider/test/PackagesRestServiceTest.java
@@ -0,0 +1,248 @@
+/*******************************************************************************
+ * Copyright (c) 2023 Eurotech and/or its affiliates and others
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Eurotech
+ *******************************************************************************/
+package org.eclipse.kura.rest.packages.provider.test;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Objects;
+
+import org.eclipse.kura.core.testutil.requesthandler.AbstractRequestHandlerTest;
+import org.eclipse.kura.core.testutil.requesthandler.RestTransport;
+import org.eclipse.kura.core.testutil.requesthandler.Transport;
+import org.eclipse.kura.core.testutil.requesthandler.Transport.MethodSpec;
+import org.eclipse.kura.deployment.agent.DeploymentAgentService;
+import org.eclipse.kura.internal.rest.deployment.agent.DeploymentRestService;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.Version;
+import org.osgi.service.deploymentadmin.DeploymentAdmin;
+import org.osgi.service.deploymentadmin.DeploymentPackage;
+
+@RunWith(Parameterized.class)
+public class PackagesRestServiceTest extends AbstractRequestHandlerTest {
+
+ private final ArrayList deploymentPackages = new ArrayList<>();
+ private Exception occurredException;
+
+ @Test
+ public void getShouldWorkWithEmptyList() {
+ givenDeploymentPackageList();
+
+ whenRequestIsPerformed(new MethodSpec("GET"), "");
+
+ thenRequestSucceeds();
+ thenNoExceptionOccurred();
+ thenResponseBodyEqualsJson("[]");
+ }
+
+ @Test
+ public void getShouldWorkWithNonEmptyList() {
+ givenDeploymentPackageWith("testPackage", "1.0.0");
+ givenDeploymentPackageWith("anotherAwesomePackage", "4.2.0");
+ givenDeploymentPackageList();
+
+ whenRequestIsPerformed(new MethodSpec("GET"), "");
+
+ thenRequestSucceeds();
+ thenNoExceptionOccurred();
+ thenResponseBodyEqualsJson(
+ "[{\"name\":\"testPackage\",\"version\":\"1.0.0\"},{\"name\":\"anotherAwesomePackage\",\"version\":\"4.2.0\"}]");
+ }
+
+ @Test
+ public void installShouldWorkWithEmptyRequest() {
+ whenRequestIsPerformed(new MethodSpec("POST"), "/_install");
+
+ thenResponseCodeIs(400);
+
+ thenNoExceptionOccurred();
+ thenInstallIsNeverCalled();
+ }
+
+ @Test
+ public void installShouldWorkWithValidURL() {
+ whenRequestIsPerformed(new MethodSpec("POST"), "/_install", "{'url':'http://localhost:8080/testPackage.dp'}");
+
+ thenRequestSucceeds();
+
+ thenNoExceptionOccurred();
+ thenInstallIsCalledWith("http://localhost:8080/testPackage.dp");
+ thenResponseBodyEqualsJson("\"REQUEST_RECEIVED\"");
+ }
+
+ @Test
+ public void installShouldWorkWithValidURLWhenARequestWasAlreadyIssued() {
+ givenAnInstallationRequestWasAlreadyIssuedFor("http://localhost:8080/testPackage.dp");
+
+ whenRequestIsPerformed(new MethodSpec("POST"), "/_install", "{'url':'http://localhost:8080/testPackage.dp'}");
+
+ thenRequestSucceeds();
+
+ thenNoExceptionOccurred();
+ thenInstallIsNeverCalled();
+ thenResponseBodyEqualsJson("\"INSTALLING\"");
+ }
+
+ @Test
+ public void uninstallShouldWorkWithValidPackageName() {
+ whenRequestIsPerformed(new MethodSpec("DELETE"), "/testPackage");
+
+ thenRequestSucceeds();
+
+ thenNoExceptionOccurred();
+ thenUninstallIsCalledWith("testPackage");
+ thenResponseBodyEqualsJson("\"REQUEST_RECEIVED\"");
+ }
+
+ @Test
+ public void uninstallShouldWorkWithValidPackageNameWhenARequestWasAlreadyIssued() {
+ givenAnUninstallationRequestWasAlreadyIssuedFor("testPackage");
+
+ whenRequestIsPerformed(new MethodSpec("DELETE"), "/testPackage");
+
+ thenRequestSucceeds();
+
+ thenNoExceptionOccurred();
+ thenUninstallIsNeverCalled();
+ thenResponseBodyEqualsJson("\"UNINSTALLING\"");
+ }
+
+ public PackagesRestServiceTest(Transport transport) {
+ super(transport);
+ Mockito.reset(deploymentAdmin);
+ Mockito.reset(deploymentAgentService);
+ }
+
+ private static DeploymentAgentService deploymentAgentService = Mockito.mock(DeploymentAgentService.class);
+ private static DeploymentAdmin deploymentAdmin = Mockito.mock(DeploymentAdmin.class);
+
+ @Parameterized.Parameters
+ public static Collection transports() {
+ return Arrays.asList(new RestTransport("deploy/v2"));
+ }
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ final Dictionary deploymentServiceProperties = new Hashtable<>();
+ deploymentServiceProperties.put("service.ranking", Integer.MIN_VALUE);
+ deploymentServiceProperties.put("kura.service.pid", "mockDeploymentService");
+
+ BundleContext packagesRestServiceContext = FrameworkUtil.getBundle(PackagesRestServiceTest.class)
+ .getBundleContext();
+ packagesRestServiceContext.registerService(DeploymentAgentService.class, deploymentAgentService,
+ deploymentServiceProperties);
+
+ // Inject mock deployment admin
+ final ServiceReference deploymentRestServiceRef = packagesRestServiceContext
+ .getServiceReference(DeploymentRestService.class);
+ if (Objects.isNull(deploymentRestServiceRef)) {
+ throw new IllegalStateException("Unable to find instance of: " + DeploymentRestService.class.getName());
+ }
+
+ final DeploymentRestService service = packagesRestServiceContext.getService(deploymentRestServiceRef);
+ if (Objects.isNull(service)) {
+ throw new IllegalStateException("Unable to get instance of: " + DeploymentRestService.class.getName());
+ }
+ service.setDeploymentAdmin(deploymentAdmin);
+
+ }
+
+ /*
+ * GIVEN
+ */
+ private void givenDeploymentPackageWith(String name, String version) {
+ DeploymentPackage dp = Mockito.mock(DeploymentPackage.class);
+ when(dp.getName()).thenReturn(name);
+ when(dp.getVersion()).thenReturn(new Version(version));
+ this.deploymentPackages.add(dp);
+ }
+
+ private void givenDeploymentPackageList() {
+ DeploymentPackage[] deploymentPackagesArray = new DeploymentPackage[this.deploymentPackages.size()];
+ deploymentPackagesArray = this.deploymentPackages.toArray(deploymentPackagesArray);
+ when(deploymentAdmin.listDeploymentPackages()).thenReturn(deploymentPackagesArray);
+ }
+
+ private void givenAnInstallationRequestWasAlreadyIssuedFor(String url) {
+ when(deploymentAgentService.isInstallingDeploymentPackage(url)).thenReturn(true);
+ }
+
+ private void givenAnUninstallationRequestWasAlreadyIssuedFor(String packageName) {
+ when(deploymentAgentService.isUninstallingDeploymentPackage(packageName)).thenReturn(true);
+ }
+
+ /*
+ * THEN
+ */
+ private void thenInstallIsCalledWith(String url) {
+ try {
+ verify(deploymentAgentService).installDeploymentPackageAsync(url);
+ } catch (Exception e) {
+ this.occurredException = e;
+ }
+ }
+
+ private void thenInstallIsNeverCalled() {
+ try {
+ verify(deploymentAgentService, never()).installDeploymentPackageAsync(anyString());
+ } catch (Exception e) {
+ this.occurredException = e;
+ }
+ }
+
+ private void thenUninstallIsCalledWith(String packageName) {
+ try {
+ verify(deploymentAgentService).uninstallDeploymentPackageAsync(packageName);
+ } catch (Exception e) {
+ this.occurredException = e;
+ }
+ }
+
+ private void thenUninstallIsNeverCalled() {
+ try {
+ verify(deploymentAgentService, never()).uninstallDeploymentPackageAsync(anyString());
+ } catch (Exception e) {
+ this.occurredException = e;
+ }
+ }
+
+ private void thenNoExceptionOccurred() {
+ String errorMessage = "Empty message";
+ if (Objects.nonNull(this.occurredException)) {
+ StringWriter sw = new StringWriter();
+ this.occurredException.printStackTrace(new PrintWriter(sw));
+
+ errorMessage = String.format("No exception expected, \"%s\" found. Caused by: %s",
+ this.occurredException.getClass().getName(), sw.toString());
+ }
+
+ assertNull(errorMessage, this.occurredException);
+ }
+}
diff --git a/kura/test/org.eclipse.kura.rest.packages.provider.test/src/test/java/org/eclipse/kura/internal/rest/deployment/agent/test/DeploymentRestServiceUnitTest.java b/kura/test/org.eclipse.kura.rest.packages.provider.test/src/test/java/org/eclipse/kura/internal/rest/deployment/agent/test/DeploymentRestServiceUnitTest.java
new file mode 100644
index 00000000000..85136d496c3
--- /dev/null
+++ b/kura/test/org.eclipse.kura.rest.packages.provider.test/src/test/java/org/eclipse/kura/internal/rest/deployment/agent/test/DeploymentRestServiceUnitTest.java
@@ -0,0 +1,282 @@
+/*******************************************************************************
+ * Copyright (c) 2023 Eurotech and/or its affiliates and others
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Eurotech
+ *******************************************************************************/
+package org.eclipse.kura.internal.rest.deployment.agent.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import javax.ws.rs.WebApplicationException;
+
+import org.eclipse.kura.KuraErrorCode;
+import org.eclipse.kura.KuraException;
+import org.eclipse.kura.deployment.agent.DeploymentAgentService;
+import org.eclipse.kura.internal.rest.deployment.agent.DeploymentPackageInfo;
+import org.eclipse.kura.internal.rest.deployment.agent.DeploymentRestService;
+import org.eclipse.kura.rest.deployment.agent.api.DeploymentRequestStatus;
+import org.eclipse.kura.rest.deployment.agent.api.InstallRequest;
+import org.junit.Test;
+import org.osgi.framework.Version;
+import org.osgi.service.deploymentadmin.DeploymentAdmin;
+import org.osgi.service.deploymentadmin.DeploymentPackage;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdmin;
+
+public class DeploymentRestServiceUnitTest {
+
+ private DeploymentRestService deploymentRestService = new DeploymentRestService();
+
+ private DeploymentRequestStatus resultingDeploymentRequestStatus;
+ private List resultingDepoloymentPackagesList;
+ private Exception occurredException;
+
+ private DeploymentAgentService mockDeploymentAgentService = mock(DeploymentAgentService.class);
+ private DeploymentAdmin mockDeploymentAdmin = mock(DeploymentAdmin.class);
+ private UserAdmin mockUserAdmin = mock(UserAdmin.class);
+
+ private final ArrayList installedDeploymentPackages = new ArrayList<>();
+
+ @Test
+ public void installDeploymentPackageWorksWithAreadyIssuedRequest() {
+ givenDeploymentRestService();
+
+ givenAnInstallationRequestWasAlreadyIssuedFor("testPackage", true);
+
+ whenAnInstallationRequestIsIssuedFor("testPackage");
+
+ thenNoExceptionOccurred();
+ thenDeploymentRequestStatusIs(DeploymentRequestStatus.INSTALLING);
+ }
+
+ @Test
+ public void uninstallDeploymentPackageWorksWithAreadyIssuedRequest() {
+ givenDeploymentRestService();
+ givenAnUninstallationRequestWasAlreadyIssuedFor("testPackage", true);
+
+ whenAnUninstallationRequestIsIssuedFor("testPackage");
+
+ thenNoExceptionOccurred();
+ thenDeploymentRequestStatusIs(DeploymentRequestStatus.UNINSTALLING);
+ }
+
+ @Test
+ public void installDeploymentPackageWorksWhenErrorIsThrown() throws Exception {
+ givenDeploymentRestService();
+ givenDeploymentAgentServiceThrowsExceptionOnInstall();
+
+ whenAnInstallationRequestIsIssuedFor("testPackage");
+
+ thenExceptionOccurred(WebApplicationException.class);
+ }
+
+ @Test
+ public void installDeploymentPackageWorksWithNullRequest() {
+ givenDeploymentRestService();
+
+ whenAnInstallationRequestIsIssuedFor(null);
+
+ thenExceptionOccurred(WebApplicationException.class);
+ }
+
+ @Test
+ public void installDeploymentPackageWorks() {
+ givenDeploymentRestService();
+
+ givenAnInstallationRequestWasAlreadyIssuedFor("testPackage", false);
+
+ whenAnInstallationRequestIsIssuedFor("testPackage");
+
+ thenNoExceptionOccurred();
+ thenDeploymentRequestStatusIs(DeploymentRequestStatus.REQUEST_RECEIVED);
+ }
+
+ @Test
+ public void uninstallDeploymentPackageWorksWhenExceptionIsThrown() throws Exception {
+ givenDeploymentRestService();
+ givenDeploymentAgentServiceThrowsExceptionOnUninstall();
+
+ whenAnUninstallationRequestIsIssuedFor("testPackage");
+
+ thenExceptionOccurred(WebApplicationException.class);
+ }
+
+ @Test
+ public void uninstallDeploymentPackageWorks() {
+ givenDeploymentRestService();
+ givenAnUninstallationRequestWasAlreadyIssuedFor("testPackage", false);
+
+ whenAnUninstallationRequestIsIssuedFor("testPackage");
+
+ thenNoExceptionOccurred();
+ thenDeploymentRequestStatusIs(DeploymentRequestStatus.REQUEST_RECEIVED);
+ }
+
+ @Test
+ public void listDeploymentPackagesWorksWithEmptyList() {
+ givenDeploymentRestService();
+ givenInstalledDeploymentPackageList();
+
+ whenListDeploymentPackagesIsCalled();
+
+ thenResponsePackageListHaveSize(0);
+ }
+
+ @Test
+ public void listDeploymentPackagesWorksWithInstalledPackages() {
+ givenDeploymentRestService();
+ givenInstalledDeploymentPackageWith("testPackage", "1.0.0");
+ givenInstalledDeploymentPackageList();
+
+ whenListDeploymentPackagesIsCalled();
+
+ thenResponsePackageListHaveSize(1);
+ thenResponsePackageListContains("testPackage", "1.0.0");
+ }
+
+ @Test
+ public void restDeployRoleGetsCreated() throws InterruptedException {
+ givenDeploymentRestService();
+
+ thenRoleIsCreated("kura.permission.rest.deploy", Role.GROUP);
+ }
+
+ /*
+ * GIVEN
+ */
+ private void givenDeploymentRestService() {
+ deploymentRestService.setDeploymentAgentService(this.mockDeploymentAgentService);
+ deploymentRestService.setDeploymentAdmin(this.mockDeploymentAdmin);
+ deploymentRestService.setUserAdmin(this.mockUserAdmin);
+ }
+
+ private void givenInstalledDeploymentPackageWith(String name, String version) {
+ DeploymentPackage dp = mock(DeploymentPackage.class);
+ when(dp.getName()).thenReturn(name);
+ when(dp.getVersion()).thenReturn(new Version(version));
+ this.installedDeploymentPackages.add(dp);
+ }
+
+ private void givenInstalledDeploymentPackageList() {
+ DeploymentPackage[] deploymentPackagesArray = new DeploymentPackage[this.installedDeploymentPackages.size()];
+ deploymentPackagesArray = this.installedDeploymentPackages.toArray(deploymentPackagesArray);
+ when(this.mockDeploymentAdmin.listDeploymentPackages()).thenReturn(deploymentPackagesArray);
+ }
+
+ private void givenAnInstallationRequestWasAlreadyIssuedFor(String url, boolean alreadyIssued) {
+ when(this.mockDeploymentAgentService.isInstallingDeploymentPackage(url)).thenReturn(alreadyIssued);
+ }
+
+ private void givenAnUninstallationRequestWasAlreadyIssuedFor(String name, boolean alreadyIssued) {
+ when(this.mockDeploymentAgentService.isUninstallingDeploymentPackage(name)).thenReturn(alreadyIssued);
+ }
+
+ private void givenDeploymentAgentServiceThrowsExceptionOnUninstall() throws Exception {
+ doThrow(new KuraException(KuraErrorCode.BAD_REQUEST)).when(this.mockDeploymentAgentService)
+ .uninstallDeploymentPackageAsync(any());
+ }
+
+ private void givenDeploymentAgentServiceThrowsExceptionOnInstall() throws Exception {
+ doThrow(new KuraException(KuraErrorCode.BAD_REQUEST)).when(this.mockDeploymentAgentService)
+ .installDeploymentPackageAsync(any());
+ }
+
+ /*
+ * WHEN
+ */
+ private void whenAnInstallationRequestIsIssuedFor(String url) {
+ try {
+ InstallRequest installRequest = new InstallRequest(url);
+ this.resultingDeploymentRequestStatus = deploymentRestService.installDeploymentPackage(installRequest);
+ } catch (Exception e) {
+ this.occurredException = e;
+ }
+ }
+
+ private void whenAnUninstallationRequestIsIssuedFor(String name) {
+ try {
+ this.resultingDeploymentRequestStatus = deploymentRestService.uninstallDeploymentPackage(name);
+ } catch (Exception e) {
+ this.occurredException = e;
+ }
+ }
+
+ private void whenListDeploymentPackagesIsCalled() {
+ try {
+ this.resultingDepoloymentPackagesList = deploymentRestService.listDeploymentPackages();
+ } catch (Exception e) {
+ this.occurredException = e;
+ }
+ }
+
+ /*
+ * THEN
+ */
+
+ private void thenNoExceptionOccurred() {
+ String errorMessage = "Empty message";
+ if (Objects.nonNull(this.occurredException)) {
+ StringWriter sw = new StringWriter();
+ this.occurredException.printStackTrace(new PrintWriter(sw));
+
+ errorMessage = String.format("No exception expected, \"%s\" found. Caused by: %s",
+ this.occurredException.getClass().getName(), sw.toString());
+ }
+
+ assertNull(errorMessage, this.occurredException);
+ }
+
+ private void thenExceptionOccurred(Class expectedException) {
+ assertNotNull(this.occurredException);
+ assertEquals(expectedException.getName(), this.occurredException.getClass().getName());
+ }
+
+ private void thenDeploymentRequestStatusIs(DeploymentRequestStatus expectedResponse) {
+ assertNotNull(this.resultingDeploymentRequestStatus);
+ assertEquals(expectedResponse, this.resultingDeploymentRequestStatus);
+ }
+
+ private void thenResponsePackageListHaveSize(int expectedSize) {
+ assertNotNull(this.resultingDepoloymentPackagesList);
+ assertEquals(expectedSize, this.resultingDepoloymentPackagesList.size());
+ }
+
+ private void thenResponsePackageListContains(String name, String version) {
+ assertNotNull(this.resultingDepoloymentPackagesList);
+
+ for (DeploymentPackageInfo dp : this.resultingDepoloymentPackagesList) {
+ if (dp.getName().equals(name) && dp.getVersion().equals(version)) {
+ return;
+ }
+ }
+
+ fail(String.format("Package %s:%s not found", name, version));
+ }
+
+ private void thenRoleIsCreated(String role, int type) {
+ verify(this.mockUserAdmin, times(1)).createRole(role, type);
+ }
+
+}
\ No newline at end of file
diff --git a/kura/test/pom.xml b/kura/test/pom.xml
index 086b41f954b..1730aa1ff0c 100644
--- a/kura/test/pom.xml
+++ b/kura/test/pom.xml
@@ -928,6 +928,7 @@
org.eclipse.kura.rest.configuration.provider.test
org.eclipse.kura.rest.command.provider.test
org.eclipse.kura.rest.inventory.provider.test
+ org.eclipse.kura.rest.packages.provider.test
org.eclipse.kura.rest.position.provider.test
org.eclipse.kura.rest.security.provider.test
org.eclipse.kura.rest.service.listing.provider.test