From 2b0c97a73b1e58420ea50c37c700f555f613ec65 Mon Sep 17 00:00:00 2001 From: jgomer2001 Date: Thu, 8 Dec 2022 09:47:36 -0500 Subject: [PATCH] feat: add endpoints for MVP ADS projects management #3094 --- .../io/jans/configapi/util/ApiConstants.java | 1 + .../jans/configapi/rest/ApiApplication.java | 1 + .../resource/auth/ADSDeploymentsResource.java | 106 +++++++++++++ .../service/auth/ADSDeploymentsService.java | 139 ++++++++++++++++++ 4 files changed, 247 insertions(+) create mode 100644 jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/ADSDeploymentsResource.java create mode 100644 jans-config-api/server/src/main/java/io/jans/configapi/service/auth/ADSDeploymentsService.java diff --git a/jans-config-api/common/src/main/java/io/jans/configapi/util/ApiConstants.java b/jans-config-api/common/src/main/java/io/jans/configapi/util/ApiConstants.java index 6b55995ce24..de26f4bef3a 100644 --- a/jans-config-api/common/src/main/java/io/jans/configapi/util/ApiConstants.java +++ b/jans-config-api/common/src/main/java/io/jans/configapi/util/ApiConstants.java @@ -78,6 +78,7 @@ private ApiConstants() {} public static final String SESSIONID_PATH = "/{sessionId}"; public static final String USERDN_PATH = "/{userDn}"; public static final String AGAMA = "/agama"; + public static final String ADS_DEPLOYMENTS = "/ads-deployment"; public static final String QNAME_PATH = "{qname}"; public static final String ENABLED = "enabled"; public static final String QNAME = "qname"; diff --git a/jans-config-api/server/src/main/java/io/jans/configapi/rest/ApiApplication.java b/jans-config-api/server/src/main/java/io/jans/configapi/rest/ApiApplication.java index 075136b86b4..9367088be0e 100644 --- a/jans-config-api/server/src/main/java/io/jans/configapi/rest/ApiApplication.java +++ b/jans-config-api/server/src/main/java/io/jans/configapi/rest/ApiApplication.java @@ -118,6 +118,7 @@ public Set> getClasses() { classes.add(HealthCheckResource.class); classes.add(OrganizationResource.class); classes.add(AgamaResource.class); + classes.add(ADSDeploymentsResource.class); classes.add(SessionResource.class); return classes; diff --git a/jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/ADSDeploymentsResource.java b/jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/ADSDeploymentsResource.java new file mode 100644 index 00000000000..77af1151f90 --- /dev/null +++ b/jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/ADSDeploymentsResource.java @@ -0,0 +1,106 @@ +package io.jans.configapi.rest.resource.auth; + +import io.jans.ads.model.Deployment; +import io.jans.orm.model.PagedResult; +import io.jans.configapi.core.rest.ProtectedApi; +import io.jans.configapi.util.ApiAccessConstants; +import io.jans.configapi.util.ApiConstants; +import io.jans.configapi.service.auth.ADSDeploymentsService; + +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path(ApiConstants.ADS_DEPLOYMENTS) +@Produces(MediaType.APPLICATION_JSON) +public class ADSDeploymentsResource extends ConfigBaseResource { + + @Inject + private ADSDeploymentsService ads; + + @GET + @Path("list") + @ProtectedApi(scopes = { ApiAccessConstants.AGAMA_READ_ACCESS }, groupScopes = { + ApiAccessConstants.AGAMA_WRITE_ACCESS }, superScopes = { ApiAccessConstants.SUPER_ADMIN_READ_ACCESS }) + @Produces(MediaType.APPLICATION_JSON) + public Response getDeployments(@QueryParam("start") int start, @QueryParam("count") int count) { + + // this is NOT a search but a paged listing + int maxcount = getMaxCount(); + PagedResult res = ads.list(start > 0 ? start - 1 : 0, count > 0 ? count : maxcount, maxcount); + res.getEntries().forEach(d -> d.getDetails().setFolders(null)); + res.setStart(start + 1); + return Response.ok(res).build(); + + } + + @GET + @ProtectedApi(scopes = { ApiAccessConstants.AGAMA_READ_ACCESS }, groupScopes = { + ApiAccessConstants.AGAMA_WRITE_ACCESS }, superScopes = { ApiAccessConstants.SUPER_ADMIN_READ_ACCESS }) + @Produces(MediaType.APPLICATION_JSON) + public Response getDeployment(@QueryParam("name") String projectName) { + + if (projectName == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Project name missing in query string").build(); + } + + Deployment d = ads.getDeployment(projectName); + + if (d == null) + return Response.status(Response.Status.NOT_FOUND) + .entity("Unknown project " + projectName).build(); + + if (d.getFinishedAt() == null) + return Response.noContent().build(); + + d.getDetails().setFolders(null); + return Response.ok(d).build(); + + } + + @POST + @Consumes("application/zip") + @ProtectedApi(scopes = { ApiAccessConstants.AGAMA_WRITE_ACCESS }, + superScopes = { ApiAccessConstants.SUPER_ADMIN_WRITE_ACCESS }) + public Response deploy(@QueryParam("name") String projectName, byte[] gamaBinary) { + + if (projectName == null || gamaBinary == null) + return Response.status(Response.Status.BAD_REQUEST) + .entity("Project name or binary data missing").build(); + + if (ads.createDeploymentTask(projectName, gamaBinary)) + return Response.accepted().entity("A deployment task for project " + projectName + + " has been queued. Use the GET endpoint to poll status").build(); + + return Response.status(Response.Status.CONFLICT) + .entity("There is an active deployment task for " + projectName + + ". Wait for an OK response from the GET endpoint").build(); + + } + + @DELETE + @ProtectedApi(scopes = { ApiAccessConstants.AGAMA_WRITE_ACCESS }, + superScopes = { ApiAccessConstants.SUPER_ADMIN_WRITE_ACCESS }) + public Response undeploy(@QueryParam("name") String projectName) { + + if (projectName == null) + return Response.status(Response.Status.BAD_REQUEST) + .entity("Project name missing in query string").build(); + + Boolean result = ads.createUndeploymentTask(projectName); + + if (result == null) + return Response.status(Response.Status.NOT_FOUND) + .entity("Unknown project " + projectName).build(); + + if (!result) + return Response.status(Response.Status.CONFLICT) + .entity("Cannot undeploy project " + projectName + ": it is currently being deployed").build(); + + return Response.noContent().build(); + + } + +} \ No newline at end of file diff --git a/jans-config-api/server/src/main/java/io/jans/configapi/service/auth/ADSDeploymentsService.java b/jans-config-api/server/src/main/java/io/jans/configapi/service/auth/ADSDeploymentsService.java new file mode 100644 index 00000000000..87a4b72f84c --- /dev/null +++ b/jans-config-api/server/src/main/java/io/jans/configapi/service/auth/ADSDeploymentsService.java @@ -0,0 +1,139 @@ +package io.jans.configapi.service.auth; + +import io.jans.ads.model.Deployment; +import io.jans.ads.model.DeploymentDetails; +import io.jans.orm.model.PagedResult; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.search.filter.Filter; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.Base64; +import java.util.Date; + +import org.slf4j.Logger; + +import static java.nio.charset.StandardCharsets.UTF_8; + +@ApplicationScoped +public class ADSDeploymentsService { + + private static final String BASE_DN = "ou=deployments,ou=agama,o=jans"; + + @Inject + private Logger logger; + + @Inject + private PersistenceEntryManager entryManager; + + public PagedResult list(int start, int count, int maxCount) { + + String[] attrs = new String[]{ "jansId", "jansStartDate", "jansActive", + "jansEndDate", "adsPrjDeplDetails" }; + Filter filter = Filter.createPresenceFilter("jansId"); + + return entryManager.findPagedEntries(BASE_DN, Deployment.class, + filter, attrs, "jansStartDate", null, start, count, maxCount); + + } + + public Deployment getDeployment(String name) { + + String[] attrs = new String[]{ "jansStartDate", "jansEndDate", "adsPrjDeplDetails" }; + logger.info("Looking up project named {}", name); + + Deployment d = null; + try { + d = entryManager.find(dnOfProject(idFromName(name)), Deployment.class, attrs); + } catch (Exception e) { + logger.warn(e.getMessage()); + } + return d; + + } + + public boolean createDeploymentTask(String name, byte[] gamaBinary) { + + Deployment d = null; + String id = idFromName(name); + try { + String[] attrs = new String[]{ "jansActive", "jansEndDate", "dn" }; + d = entryManager.find(dnOfProject(id), Deployment.class, attrs); + } catch (Exception e) { + logger.debug("No already existing deployment for project {}", name); + } + + boolean existing = d != null; + if (existing && d.getFinishedAt() == null) { + logger.info("A deployment is still in course for this project!"); + + if (!d.isTaskActive()) { + logger.info("No node is in charge of this task yet"); + } + return false; + } + + DeploymentDetails dd = new DeploymentDetails(); + dd.setProjectName(name); + + if (!existing) { + d = new Deployment(); + d.setDn(dnOfProject(id)); + } + d.setId(id); + d.setCreatedAt(new Date()); + d.setTaskActive(false); + d.setFinishedAt(null); + d.setDetails(dd); + + byte[] encoded = Base64.getEncoder().encode(gamaBinary); + d.setAssets(new String(encoded, UTF_8)); + + logger.info("Persisting deployment task for project {}", name); + if (existing) { + entryManager.merge(d); + } else { + entryManager.persist(d); + } + return true; + + } + + public Boolean createUndeploymentTask(String name) { + + String dn = dnOfProject(idFromName(name)); + String[] attrs = new String[]{ "jansActive", "dn" }; + Deployment d = null; + + try { + d = entryManager.find(dn, Deployment.class, attrs); + } catch (Exception e) { + logger.warn(e.getMessage()); + } + + if (d == null) return null; + + //A project can be undeployed when there is no deployment in course or + //when there is but no node has taken it yet + boolean deploymentInProcess = d.isTaskActive(); + if (!deploymentInProcess) { + logger.info("Removing deployment of project {}", name); + entryManager.remove(dn, Deployment.class); + } + + return !deploymentInProcess; + + } + + private static String dnOfProject(String prjId) { + return String.format("jansId=%s,%s", prjId, BASE_DN); + } + + private static String idFromName(String name) { + String hash = Integer.toString(name.hashCode()); + if (hash.startsWith("-")) hash = hash.substring(1); + return hash; + } + +} \ No newline at end of file