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