From 984eaaeaa8c583b687a1eedbb4dbdbdedb74653f Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Tue, 3 Jan 2023 15:35:43 -0600 Subject: [PATCH 01/49] Bump to 1.0.0-SNAPSHOT --- build.gradle | 2 +- .../org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 00358ec..7d77fa8 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "0.3.0" +version "1.0.0-SNAPSHOT" description "JupyterHub Plugin for XNAT 1.8.6" repositories { diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java index f51f736..0eec288 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java @@ -3,6 +3,7 @@ import org.mockito.Mockito; import org.nrg.framework.configuration.ConfigPaths; import org.nrg.framework.services.NrgEventServiceI; +import org.nrg.framework.services.SerializerService; import org.nrg.framework.utilities.OrderedProperties; import org.nrg.prefs.services.NrgPreferenceService; import org.nrg.xdat.preferences.SiteConfigPreferences; @@ -131,4 +132,9 @@ public XFTManagerHelper mockXFTManagerHelper() { public EventTrackingDataHibernateService mockEventTrackingDataHibernateService() { return Mockito.mock(EventTrackingDataHibernateService.class); } + + @Bean + public SerializerService serializerService() { + return Mockito.mock(SerializerService.class); + } } From 398945800d5adbc74bac0a0beb887bbfd43cbcc7 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Tue, 3 Jan 2023 15:40:58 -0600 Subject: [PATCH 02/49] Hide 'Start Jupyter' in stored searches. This should have been removed before 0.3.0. --- .../search/plugins/pre/StartJupyterServer.vm | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/resources/META-INF/resources/templates/screens/search/plugins/pre/StartJupyterServer.vm b/src/main/resources/META-INF/resources/templates/screens/search/plugins/pre/StartJupyterServer.vm index 8f20b9a..0c1daa5 100644 --- a/src/main/resources/META-INF/resources/templates/screens/search/plugins/pre/StartJupyterServer.vm +++ b/src/main/resources/META-INF/resources/templates/screens/search/plugins/pre/StartJupyterServer.vm @@ -42,13 +42,13 @@ jupyterHandler = jupyterHandlerForProjectBundleSearch; } - if (jupyterHandler) { - let jupyterMenuOption = { - 'label' : 'Start Jupyter', - 'handler': jupyterHandler - } - - addSearchMenuOption(jupyterMenuOption) - } + // if (jupyterHandler) { + // let jupyterMenuOption = { + // 'label' : 'Start Jupyter', + // 'handler': jupyterHandler + // } + // + // addSearchMenuOption(jupyterMenuOption) + // } \ No newline at end of file From 67ac11357f4a6b2e2ffab7964fca4732dc79e55b Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Tue, 3 Jan 2023 15:45:49 -0600 Subject: [PATCH 03/49] JHP-34: Restrict Access to Jupyter --- .../plugins/jupyterhub/JupyterHubPlugin.java | 1 + .../JupyterUserAuthorization.java | 46 +++ .../preferences/JupyterHubPreferences.java | 14 + .../jupyterhub/rest/JupyterHubApi.java | 13 +- .../rest/JupyterHubPreferencesApi.java | 4 + .../jupyterhub/jupyterhub-preferences.js | 25 +- .../plugin/jupyterhub/jupyterhub-user-auth.js | 293 ++++++++++++++++++ .../templates/screens/topBar/Jupyter.vm | 10 +- .../screens/topBar/Jupyter/Default.vm | 1 + .../actionsBox/StartJupyterServer.vm | 10 +- .../actionsBox/StartJupyterServer.vm | 10 +- .../actionsBox/StartJupyterServer.vm | 10 +- .../spawner/jupyterhub/site-settings.yaml | 36 +++ .../JupyterUserAuthorizationTest.java | 104 +++++++ .../config/JupyterAuthorizationConfig.java | 20 ++ 15 files changed, 582 insertions(+), 15 deletions(-) create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java create mode 100644 src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-user-auth.js create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterAuthorizationConfig.java diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java index dcd91f0..78ee87c 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java @@ -32,6 +32,7 @@ "org.nrg.xnatx.plugins.jupyterhub.listeners", "org.nrg.xnatx.plugins.jupyterhub.repositories", "org.nrg.xnatx.plugins.jupyterhub.utils", + "org.nrg.xnatx.plugins.jupyterhub.authorization", "org.nrg.xnatx.plugins.jupyterhub.initialize"}) @Slf4j public class JupyterHubPlugin { diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java new file mode 100644 index 0000000..55e28bc --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java @@ -0,0 +1,46 @@ +package org.nrg.xnatx.plugins.jupyterhub.authorization; + +import org.aspectj.lang.JoinPoint; +import org.nrg.xapi.authorization.UserXapiAuthorization; +import org.nrg.xdat.security.helpers.AccessLevel; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xft.security.UserI; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; + +@Component +public class JupyterUserAuthorization extends UserXapiAuthorization { + + private static final String JUPYTER_ROLE = "Jupyter"; + + private final RoleHolder roleHolder; + private final JupyterHubPreferences jupyterHubPreferences; + + public JupyterUserAuthorization(final RoleHolder roleHolder, + final JupyterHubPreferences jupyterHubPreferences) { + this.roleHolder = roleHolder; + this.jupyterHubPreferences = jupyterHubPreferences; + } + + @Override + protected boolean checkImpl(AccessLevel accessLevel, JoinPoint joinPoint, UserI user, HttpServletRequest request) { + boolean userAuth = super.checkImpl(accessLevel, joinPoint, user, request); + boolean jupyterAuth = checkJupyter(user); + + return userAuth && jupyterAuth; + } + + @Override + protected boolean considerGuests() { + return false; + } + + protected boolean checkJupyter(UserI user) { + return jupyterHubPreferences.getAllUsersCanStartJupyter() + || roleHolder.checkRole(user, JUPYTER_ROLE) + || roleHolder.isSiteAdmin(user); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java index 00bef81..4949c91 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java @@ -22,6 +22,7 @@ public class JupyterHubPreferences extends AbstractPreferenceBean { public static final String TOOL_ID = "jupyterhub"; + public static final String ALL_USERS_JUPYTER = "allUsersCanStartJupyter"; public static final String DOCKER_IMAGES_PREF_ID = "dockerImages"; public static final String CONTAINER_SPEC_LABELS_PREF_ID = "containerSpecLabels"; public static final String PLACEMENT_SPEC_CONSTRAINTS_PREF_ID = "placementSpecConstraints"; @@ -260,4 +261,17 @@ public void setInactivityTimeout(final long inactivityTimeout) { } } + @NrgPreference(defaultValue = "false") + public boolean getAllUsersCanStartJupyter() { + return getBooleanValue(ALL_USERS_JUPYTER); + } + + public void setAllUsersCanStartJupyter(final boolean allUsersCanStartJupyter) { + try { + setBooleanValue(allUsersCanStartJupyter, ALL_USERS_JUPYTER); + } catch (InvalidPreferenceName e) { + log.error("Invalid preference name 'allUsersCanStartJupyter': something is very wrong here.", e); + } + } + } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java index 0f2a366..4258008 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java @@ -4,10 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.nrg.framework.annotations.XapiRestController; import org.nrg.xapi.exceptions.NotFoundException; -import org.nrg.xapi.rest.AbstractXapiRestController; -import org.nrg.xapi.rest.AuthorizedRoles; -import org.nrg.xapi.rest.Username; -import org.nrg.xapi.rest.XapiRequestMapping; +import org.nrg.xapi.rest.*; import org.nrg.xdat.security.helpers.AccessLevel; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; @@ -16,6 +13,7 @@ import org.nrg.xft.security.UserI; import org.nrg.xnat.tracking.entities.EventTrackingData; import org.nrg.xnat.tracking.services.EventTrackingDataHibernateService; +import org.nrg.xnatx.plugins.jupyterhub.authorization.JupyterUserAuthorization; import org.nrg.xnatx.plugins.jupyterhub.client.models.Hub; import org.nrg.xnatx.plugins.jupyterhub.client.models.Server; import org.nrg.xnatx.plugins.jupyterhub.client.models.Token; @@ -32,6 +30,7 @@ import java.util.List; import java.util.Map; +import static org.nrg.xdat.security.helpers.AccessLevel.Authorizer; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.web.bind.annotation.RequestMethod.*; @@ -137,7 +136,8 @@ public Server getNamedServer(@ApiParam(value = "username", required = true) @Pat @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized."), @ApiResponse(code = 500, message = "Unexpected error")}) - @XapiRequestMapping(value = "/users/{username}/server", method = POST, restrictTo = AccessLevel.User) + @XapiRequestMapping(value = "/users/{username}/server", method = POST, restrictTo = Authorizer) + @AuthDelegate(JupyterUserAuthorization.class) public void startServer(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, @ApiParam(value = "xsiType", required = true) @RequestParam("xsiType") final String xsiType, @ApiParam(value = "itemId", required = true) @RequestParam("itemId") final String itemId, @@ -155,7 +155,8 @@ public void startServer(@ApiParam(value = "username", required = true) @PathVari @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized."), @ApiResponse(code = 500, message = "Unexpected error")}) - @XapiRequestMapping(value = "/users/{username}/server/{servername}", method = POST, restrictTo = AccessLevel.User) + @XapiRequestMapping(value = "/users/{username}/server/{servername}", method = POST, restrictTo = Authorizer) + @AuthDelegate(JupyterUserAuthorization.class) public void startNamedServer(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, @ApiParam(value = "servername", required = true) @PathVariable("servername") final String servername, @ApiParam(value = "xsiType", required = true) @RequestParam("xsiType") final String xsiType, diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java index cc2cec6..07ce67d 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java @@ -143,6 +143,10 @@ public Map getSpecifiedPreference(@ApiParam(value = "The Jupyter value = jupyterHubPreferences.getInactivityTimeout(); break; } + case (JupyterHubPreferences.ALL_USERS_JUPYTER): { + value = jupyterHubPreferences.getAllUsersCanStartJupyter(); + break; + } default: value = jupyterHubPreferences.get(preference); } diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-preferences.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-preferences.js index 49daa33..d817bd1 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-preferences.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-preferences.js @@ -23,7 +23,7 @@ XNAT.plugin.jupyterhub.preferences = getObject(XNAT.plugin.jupyterhub.preference } }(function() { - XNAT.plugin.jupyterhub.preferences.getAll = async function (timeout = 1000) { + XNAT.plugin.jupyterhub.preferences.getAll = async function(timeout = 1000) { console.debug(`jupyterhub-preferences.js: XNAT.plugin.jupyterhub.preferences.getAll`); let url = XNAT.url.restUrl('/xapi/jupyterhub/preferences'); @@ -40,4 +40,27 @@ XNAT.plugin.jupyterhub.preferences = getObject(XNAT.plugin.jupyterhub.preference return await response.json(); } + XNAT.plugin.jupyterhub.preferences.get = async function(preference, timeout = 1000) { + console.debug(`jupyterhub-preferences.js: XNAT.plugin.jupyterhub.preferences.get`); + + let preferences = await XNAT.plugin.jupyterhub.preferences.getAll(timeout); + return preferences[preference]; + } + + XNAT.plugin.jupyterhub.preferences.set = async function(preference, value, timeout = 1000) { + console.debug(`jupyterhub-preferences.js: XNAT.plugin.jupyterhub.preferences.set`); + + let url = XNAT.url.restUrl(`/xapi/jupyterhub/preferences/${preference}`); + const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(url, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(value), + timeout: timeout, + }) + + if (!response.ok) { + throw new Error(`Error setting preference ${preference}`); + } + } + })) \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-user-auth.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-user-auth.js new file mode 100644 index 0000000..3fa7fe3 --- /dev/null +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-user-auth.js @@ -0,0 +1,293 @@ +/*! + * JupyterHub user authorization functions + */ + +console.debug('jupyterhub-user-auth.js'); + +var XNAT = getObject(XNAT || {}); +XNAT.plugin = getObject(XNAT.plugin || {}); +XNAT.plugin.jupyterhub = getObject(XNAT.plugin.jupyterhub || {}); +XNAT.plugin.jupyterhub.users = getObject(XNAT.plugin.jupyterhub.users || {}); +XNAT.plugin.jupyterhub.users.authorization = getObject(XNAT.plugin.jupyterhub.users.authorization || {}); + +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + return factory(); + } +}(function () { + + XNAT.plugin.jupyterhub.users.authorization.isAuthorized = async function (username = window.username, + timeout = 1000) { + console.debug(`XNAT.plugin.jupyterhub.users.authorization.isAuthorized`); + + if (username === 'guest') { + return false; + } + + const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users/${username}/roles`, { + method: 'GET', + headers: {'Content-Type': 'application/json'}, + timeout: timeout, + }) + + if (!response.ok) { + throw new Error(`HTTP error getting user roles for user ${username}`); + } + + let roles = await response.json(); + roles = roles.map(role => role.toLowerCase()); + + if (roles.contains('administrator') || roles.contains('jupyter')) { + return true; + } + + let preferences = await XNAT.plugin.jupyterhub.preferences.getAll(); + return preferences['allUsersCanStartJupyter']; + } + + XNAT.plugin.jupyterhub.users.authorization.getAuthorized = async function (timeout = 1000) { + console.debug(`XNAT.plugin.jupyterhub.users.authorization.getAuthorized`); + + const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users/roles/Jupyter`, { + method: 'GET', + headers: {'Content-Type': 'application/json'}, + timeout: timeout, + }) + + if (!response.ok) { + throw new Error(`HTTP error getting user roles for user ${username}`); + } + + return await response.json(); + } + + XNAT.plugin.jupyterhub.users.authorization.getUnauthorized = async function (timeout = 1000) { + console.debug(`XNAT.plugin.jupyterhub.users.authorization.getUnauthorized`); + + let authorizedUsers = await XNAT.plugin.jupyterhub.users.authorization.getAuthorized(); + + const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users`, { + method: 'GET', + headers: {'Content-Type': 'application/json'}, + timeout: timeout, + }) + + if (!response.ok) { + throw new Error(`HTTP error getting user roles for user ${username}`); + } + + let allUsers = await response.json(); + + return allUsers.filter(user => !authorizedUsers.contains(user) && user !== 'jupyterhub' && user !== 'guest'); + } + + XNAT.plugin.jupyterhub.users.authorization.add = async function (username, timeout = 1000) { + console.debug(`XNAT.plugin.jupyterhub.users.authorization.add`); + + const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users/${username}/roles`, { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(['Jupyter']), + timeout: timeout, + }) + + if (!response.ok) { + throw new Error(`Error adding jupyter role to user ${username}`); + } + } + + XNAT.plugin.jupyterhub.users.authorization.remove = async function (username, timeout = 1000) { + console.debug(`XNAT.plugin.jupyterhub.users.authorization.remove`); + + const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users/${username}/roles`, { + method: 'DELETE', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(['Jupyter']), + timeout: timeout, + }) + + if (!response.ok) { + throw new Error(`Error adding jupyter role to user ${username}`); + } + } + + XNAT.plugin.jupyterhub.users.authorization.refreshTable = function (containerId) { + console.debug(`XNAT.plugin.jupyterhub.users.authorization.refreshTable`); + + const containerEl = document.getElementById(containerId); + containerEl.innerHTML = ''; + + function removeButton(user) { + return spawn('button.btn.sm.delete', { + onclick: function () { + XNAT.plugin.jupyterhub.users.authorization.remove(user).then(() => { + XNAT.ui.banner.top(1500, '' + user + ' removed from Jupyter users list.', 'success'); + XNAT.plugin.jupyterhub.users.authorization.refreshTable(containerId); + }).catch(() => { + XNAT.ui.banner.top(2000, 'Failed to remove ' + user + ' from Jupyter users list.', 'error'); + XNAT.plugin.jupyterhub.users.authorization.refreshTable(containerId); + }); + }, + title: "Remove User", + }, [spawn('i.fa.fa-ban')]); + } + + XNAT.plugin.jupyterhub.users.authorization.getAuthorized().then(users => { + users.sort(); + + const noUsers = users.length === 0; + if (noUsers) { + users.push('No users have been authorized.'); + } + + XNAT.table.dataTable(users, { + header: true, + sortable: 'user', + columns: { + user: { + label: 'User', + apply: function () { + return this.toString(); + } + }, + actions: { + label: 'Actions', + th: {style: {width: '150px'}}, + apply: function () { + return noUsers ? '' : spawn('div.center', [removeButton(this.toString())]); + } + } + } + }).render(`#${containerId}`) + }) + } + + XNAT.plugin.jupyterhub.users.authorization.initTable = function (containerId) { + console.debug(`XNAT.plugin.jupyterhub.users.authorization.initTable`); + + const containerEl = document.getElementById(containerId); + const allowAllEl = document.getElementById('allowAll'); + const hrEl = document.getElementById('jupyter-user-auth-hr'); + + // Place 'Add User' button in the panel footer + let footerEl = containerEl.parentElement.parentElement.querySelector(".panel-footer") + + const button = spawn('button.btn.btn-sm', { + html: 'Add User', + style: { + display: 'none' + }, + id: 'add-jupyter-user-btn', + onclick: function () { + XNAT.dialog.open({ + title: 'Add Jupyter Users', + content: spawn('form'), + width: 600, + beforeShow: function (obj) { + const formContainer$ = obj.$modal.find('.xnat-dialog-content'); + formContainer$.addClass('panel'); + XNAT.plugin.jupyterhub.users.authorization.getUnauthorized().then(users => { + obj.$modal.find('form').append( + spawn('!', [ + XNAT.ui.panel.select.multiple({ + name: 'jupyter-users-to-authorize', + id: 'jupyter-users-to-authorize', + label: 'Select Users', + options: users + }).element + ]) + ); + + const selectEl = document.getElementById('jupyter-users-to-authorize'); + selectEl.style.minWidth = '175px'; + selectEl.style.minHeight = '125px'; + }) + }, + buttons: [ + { + label: 'Submit', + isDefault: true, + close: false, + action: function () { + const newUsersEl = document.getElementById("jupyter-users-to-authorize"); + const newUsers = [] + + for (let option of newUsersEl.options) { + if (option.selected) { + newUsers.push(option.value); + } + } + + Promise.allSettled(newUsers.map(user => { + XNAT.plugin.jupyterhub.users.authorization.add(user); + })).then(results => { + let bannerMessage, bannerClass; + let fulfilled = results.map(result => result.status).filter(status => status === 'fulfilled'); + + if (fulfilled.length === newUsers.length) { + if (newUsers.length === 1) { + bannerMessage = '' + newUsers[0] + ' added to Jupyter users list.'; + bannerClass = 'success'; + } else if (newUsers.length > 1) { + bannerMessage = '' + newUsers.length + ' users added to Jupyter users list.'; + bannerClass = 'success'; + } + } else { + bannerMessage = '' + fulfilled.length + '/' + newUsers.length + ' users added to Jupyter users list.' + bannerClass = 'warning'; + } + + XNAT.ui.banner.top(2000, bannerMessage, bannerClass); + xmodal.closeAll(); + XNAT.ui.dialog.closeAll(); + }).then(() => { + setTimeout(() => { + XNAT.plugin.jupyterhub.users.authorization.refreshTable(containerId); + }, 300); + }) + } + }, + { + label: 'Cancel', + close: true + } + ] + }) + } + }); + + footerEl.append(spawn('div.pull-right', [button])); + footerEl.append(spawn('div.clear.clearFix')); + + XNAT.plugin.jupyterhub.preferences.get('allUsersCanStartJupyter') + .then(preference => { + // Set allow all check box + allowAllEl.checked = preference; + + if (!allowAllEl.checked) { + button.style.display = ''; + hrEl.style.display = ''; + XNAT.plugin.jupyterhub.users.authorization.refreshTable(containerId); + } + + // Only display user table when all users are not enabled + allowAllEl.addEventListener('change', () => { + XNAT.plugin.jupyterhub.preferences.set('allUsersCanStartJupyter', allowAllEl.checked); + + if (allowAllEl.checked) { + containerEl.innerHTML = ''; + button.style.display = 'none'; + hrEl.style.display = 'none'; + } else { + button.style.display = ''; + hrEl.style.display = ''; + XNAT.plugin.jupyterhub.users.authorization.refreshTable(containerId); + } + }); + }) + } +})); \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter.vm b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter.vm index d15f862..1d2c353 100644 --- a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter.vm +++ b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter.vm @@ -1,6 +1,12 @@ -
  • Jupyter +
    • #addGlobalCustomScreens("topBar/Jupyter")
    -
  • \ No newline at end of file + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm index 414248b..7bf1bb7 100644 --- a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm +++ b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm @@ -10,5 +10,6 @@ + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/screens/xnat_imageSessionData/actionsBox/StartJupyterServer.vm b/src/main/resources/META-INF/resources/templates/screens/xnat_imageSessionData/actionsBox/StartJupyterServer.vm index b89d57d..29ff729 100644 --- a/src/main/resources/META-INF/resources/templates/screens/xnat_imageSessionData/actionsBox/StartJupyterServer.vm +++ b/src/main/resources/META-INF/resources/templates/screens/xnat_imageSessionData/actionsBox/StartJupyterServer.vm @@ -1,5 +1,11 @@
  • - + -
  • \ No newline at end of file + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/screens/xnat_projectData/actionsBox/StartJupyterServer.vm b/src/main/resources/META-INF/resources/templates/screens/xnat_projectData/actionsBox/StartJupyterServer.vm index 04cf510..c87f6c8 100644 --- a/src/main/resources/META-INF/resources/templates/screens/xnat_projectData/actionsBox/StartJupyterServer.vm +++ b/src/main/resources/META-INF/resources/templates/screens/xnat_projectData/actionsBox/StartJupyterServer.vm @@ -1,5 +1,11 @@
  • - + -
  • \ No newline at end of file + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/screens/xnat_subjectData/actionsBox/StartJupyterServer.vm b/src/main/resources/META-INF/resources/templates/screens/xnat_subjectData/actionsBox/StartJupyterServer.vm index b9e2d5e..2b3cf1b 100644 --- a/src/main/resources/META-INF/resources/templates/screens/xnat_subjectData/actionsBox/StartJupyterServer.vm +++ b/src/main/resources/META-INF/resources/templates/screens/xnat_subjectData/actionsBox/StartJupyterServer.vm @@ -1,5 +1,11 @@
  • - + -
  • \ No newline at end of file + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index 5390435..732b4aa 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -152,6 +152,41 @@ jupyterHubSetup: content: > XNAT.plugin.jupyterhub.hub.initSetupTable('jupyterhub-setup-table'); +userAuthorization: + kind: panel + name: userAuthorization + label: User Authorization + contents: + description: + tag: div.warning + element: + style: + marginBottom: 24px + html: + Users with access to Jupyter will be able to run arbitrary code from within their Jupyter notebook container. + Individual users may be authorized to start Jupyter containers or all users (excluding guest users) can be + authorized to start Jupyter containers. + allowAllToggle: + kind: panel.input.switchbox + label: "All Users Can Start Jupyter" + name: allowAll + id: allowAll + onText: false + offText: false + hr: + tag: hr#jupyter-user-auth-hr + element: + style: + display: none + jupyterUserAuthorizationPanel: + tag: "div#jupyterhub-user-auth-container" + jupyterUserAuthorizationScript: + tag: script|src="~/scripts/xnat/plugin/jupyterhub/jupyterhub-user-auth.js" + renderJupyterUserAuthorizationPanel: + tag: script + content: > + XNAT.plugin.jupyterhub.users.authorization.initTable('jupyterhub-user-auth-container') + imagePreferences: kind: panel name: imagePreferences @@ -232,6 +267,7 @@ siteSettings: active: true contents: ${jupyterHubSetup} + ${userAuthorization} ${imagePreferences} jupyterhubUserActivityTab: kind: tab diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java new file mode 100644 index 0000000..f181dfc --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java @@ -0,0 +1,104 @@ +package org.nrg.xnatx.plugins.jupyterhub.authorization; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xft.security.UserI; +import org.nrg.xnatx.plugins.jupyterhub.config.JupyterAuthorizationConfig; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = JupyterAuthorizationConfig.class) +public class JupyterUserAuthorizationTest { + + @Autowired private JupyterUserAuthorization jupyterUserAuthorization; + @Autowired private RoleServiceI mockRoleService; + @Autowired private JupyterHubPreferences mockJupyterHubPreferences; + + private UserI user; + private String username; + + @Before + public void before() { + // Mock the user + user = mock(UserI.class); + username = "user"; + when(user.getUsername()).thenReturn(username); + } + + @After + public void after() { + Mockito.reset(mockRoleService, mockJupyterHubPreferences); + } + + @Test + @Ignore + public void testCheckImpl() { + // Tough to test this. super.checkImpl(...) is protected so can't spy with Mockito. + // If you decouple JupyterUserAuthorization from UserXapiAuthorization then you have to deal with + // the JoinPoint in checkImpl(...) + } + + @Test + public void testJupyter_AllUsers() { + // Setup + when(mockJupyterHubPreferences.getAllUsersCanStartJupyter()).thenReturn(true); + when(mockRoleService.checkRole(any(), eq("Jupyter"))).thenReturn(false); + when(mockRoleService.isSiteAdmin(user)).thenReturn(false); + + // Test + boolean check = jupyterUserAuthorization.checkJupyter(user); + + // Verify + assertTrue("Jupyter All User preference is enabled. This should allow all.", check); + } + + @Test + public void testJupyter_JupyterUsers() { + // Setup + when(mockJupyterHubPreferences.getAllUsersCanStartJupyter()).thenReturn(false); + when(mockRoleService.checkRole(any(), eq("Jupyter"))).thenReturn(true); + when(mockRoleService.isSiteAdmin(user)).thenReturn(false); + + // Test + boolean check = jupyterUserAuthorization.checkJupyter(user); + + // Verify + assertTrue("Jupyter All User preference is enabled. This should allow all.", check); + } + + @Test + public void testJupyter_Admin() { + // Setup + when(mockJupyterHubPreferences.getAllUsersCanStartJupyter()).thenReturn(false); + when(mockRoleService.checkRole(any(), eq("Jupyter"))).thenReturn(false); + when(mockRoleService.isSiteAdmin(user)).thenReturn(true); + + // Test + boolean check = jupyterUserAuthorization.checkJupyter(user); + + // Verify + assertTrue("Jupyter All User preference is enabled. This should allow all.", check); + } + + @Test + public void testGuestUserNeverAuthorized() { + boolean check = jupyterUserAuthorization.considerGuests(); + assertFalse("Guest users should not be authorized", check); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterAuthorizationConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterAuthorizationConfig.java new file mode 100644 index 0000000..ebd3011 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterAuthorizationConfig.java @@ -0,0 +1,20 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xnatx.plugins.jupyterhub.authorization.JupyterUserAuthorization; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({MockConfig.class}) +public class JupyterAuthorizationConfig { + + @Bean + public JupyterUserAuthorization defaultJupyterAuthorization(final RoleHolder mockRoleHolder, + final JupyterHubPreferences mockJupyterHubPreferences) { + return new JupyterUserAuthorization(mockRoleHolder, mockJupyterHubPreferences); + } + +} From 666e0a002920aa3765bb4abbf1ab9b09dc7577a1 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Thu, 23 Feb 2023 16:27:27 -0600 Subject: [PATCH 04/49] JHP-20: Replaces Docker image selection with Profile concept when starting Jupyter notebook containers --- .../jupyterhub/entities/ProfileEntity.java | 58 ++ .../entities/UserOptionsEntity.java | 167 +---- .../JupyterHubProfileInitializer.java | 95 +++ .../initialize/JupyterHubUserInitializer.java | 2 +- .../plugins/jupyterhub/models/BindMount.java | 23 - .../jupyterhub/models/ContainerSpec.java | 20 - .../jupyterhub/models/DockerImage.java | 1 + .../jupyterhub/models/PlacementSpec.java | 18 - .../plugins/jupyterhub/models/Profile.java | 31 + .../jupyterhub/models/XnatUserOptions.java | 11 +- .../models/docker/ContainerSpec.java | 28 + .../jupyterhub/models/docker/Mount.java | 28 + .../jupyterhub/models/docker/Placement.java | 24 + .../Resources.java} | 13 +- .../models/docker/TaskTemplate.java | 25 + .../preferences/JupyterHubPreferences.java | 14 + .../jupyterhub/repositories/ProfileDao.java | 12 + .../jupyterhub/rest/JupyterHubApi.java | 8 +- .../rest/JupyterHubPreferencesApi.java | 28 - .../rest/JupyterHubProfilesApi.java | 120 ++++ .../services/JupyterHubService.java | 4 +- .../services/ProfileEntityService.java | 8 + .../jupyterhub/services/ProfileService.java | 18 + .../services/UserOptionsService.java | 2 +- .../impl/DefaultJupyterHubService.java | 55 +- .../services/impl/DefaultProfileService.java | 162 +++++ .../impl/DefaultUserOptionsService.java | 84 ++- .../impl/HibernateProfileEntityService.java | 18 + .../plugin/jupyterhub/jupyterhub-profiles.js | 645 ++++++++++++++++++ .../plugin/jupyterhub/jupyterhub-servers.js | 160 +++-- .../screens/topBar/Jupyter/Default.vm | 2 +- .../spawner/jupyterhub/site-settings.yaml | 89 +-- .../DefaultJupyterHubServiceConfig.java | 7 +- .../config/DefaultProfileServiceConfig.java | 18 + .../DefaultUserOptionsServiceConfig.java | 7 +- .../JupyterHubProfileInitializerConfig.java | 20 + .../config/JupyterHubProfilesApiConfig.java | 44 ++ .../plugins/jupyterhub/config/MockConfig.java | 17 +- .../JupyterHubProfileInitializerTest.java | 76 +++ .../jupyterhub/rest/JupyterHubApiTest.java | 57 +- .../rest/JupyterHubProfilesApiTest.java | 318 +++++++++ .../impl/DefaultJupyterHubServiceTest.java | 65 +- .../impl/DefaultProfileServiceTest.java | 285 ++++++++ 43 files changed, 2391 insertions(+), 496 deletions(-) create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/ProfileEntity.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/BindMount.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ContainerSpec.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/PlacementSpec.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Profile.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/ContainerSpec.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/Mount.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/Placement.java rename src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/{ResourceSpec.java => docker/Resources.java} (50%) create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/TaskTemplate.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/ProfileDao.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApi.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileEntityService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateProfileEntityService.java create mode 100644 src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-profiles.js create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultProfileServiceConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfileInitializerConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfilesApiConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializerTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApiTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileServiceTest.java diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/ProfileEntity.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/ProfileEntity.java new file mode 100644 index 0000000..bcf92c7 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/ProfileEntity.java @@ -0,0 +1,58 @@ +package org.nrg.xnatx.plugins.jupyterhub.entities; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.annotations.Type; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; +import org.nrg.xnatx.plugins.jupyterhub.models.Profile; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; + +@Entity +@ToString +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Slf4j +public class ProfileEntity extends AbstractHibernateEntity { + + private Profile profile; + + @Type(type = "jsonb") + @Column(columnDefinition = "jsonb", nullable = false) + @Basic(fetch=FetchType.EAGER) + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Profile toPojo() { + this.profile.setId(this.getId()); + return this.profile; + } + + public static ProfileEntity fromPojo(final Profile pojo) { + ProfileEntity profileEntity = ProfileEntity.builder() + .profile(pojo) + .build(); + + if (pojo.getId() != null) { + profileEntity.setId(pojo.getId()); + } + + return profileEntity; + } + + public ProfileEntity update(final ProfileEntity update) { + update.getProfile().setId(this.getId()); + this.setProfile(update.getProfile()); + return this; + } +} \ No newline at end of file diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/UserOptionsEntity.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/UserOptionsEntity.java index 56e2e4c..b3944d1 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/UserOptionsEntity.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/UserOptionsEntity.java @@ -1,17 +1,16 @@ package org.nrg.xnatx.plugins.jupyterhub.entities; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.type.CollectionType; import lombok.*; import lombok.extern.slf4j.Slf4j; +import org.hibernate.annotations.Type; import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.xdat.XDAT; -import org.nrg.xnatx.plugins.jupyterhub.models.*; +import org.nrg.xnatx.plugins.jupyterhub.models.XnatUserOptions; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; -import javax.persistence.*; -import java.io.IOException; -import java.util.List; -import java.util.Map; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; @Entity @ToString @@ -30,11 +29,17 @@ public class UserOptionsEntity extends AbstractHibernateEntity { private String projectId; private String eventTrackingId; - private Map environmentVariables; - private String bindMountsJson; // List serialized to json string - private String containerSpecJson; // container spec serialized to json string - private String placementSpecJson; // container spec serialized to json string - private String resourceSpecJson; // container spec serialized to json string + private TaskTemplate taskTemplate; + + @Type(type = "jsonb") + @Column(columnDefinition = "jsonb") + public TaskTemplate getTaskTemplate() { + return taskTemplate; + } + + public void setTaskTemplate(TaskTemplate taskTemplate) { + this.taskTemplate = taskTemplate; + } public Integer getUserId() { return userId; @@ -89,130 +94,6 @@ public void setEventTrackingId(String trackingId) { this.eventTrackingId = trackingId; } - @ElementCollection(fetch = FetchType.EAGER) - public Map getEnvironmentVariables() { - return environmentVariables; - } - - public void setEnvironmentVariables(Map environmentVariables) { - this.environmentVariables = environmentVariables; - } - - @Column(columnDefinition = "TEXT") - public String getBindMountsJson() { - return bindMountsJson; - } - - public void setBindMountsJson(String bindMountsJson) { - this.bindMountsJson = bindMountsJson; - } - - @Column(columnDefinition = "TEXT") - public String getContainerSpecJson() { - return containerSpecJson; - } - - public void setContainerSpecJson(String containerSpecJson) { - this.containerSpecJson = containerSpecJson; - } - - @Column(columnDefinition = "TEXT") - public String getPlacementSpecJson() { - return placementSpecJson; - } - - public void setPlacementSpecJson(String placementSpecJson) { - this.placementSpecJson = placementSpecJson; - } - - @Column(columnDefinition = "TEXT") - public String getResourceSpecJson() { - return resourceSpecJson; - } - - public void setResourceSpecJson(String resourceSpecJson) { - this.resourceSpecJson = resourceSpecJson; - } - - - public static String bindMountPojo(List bindMounts) { - try { - return XDAT.getSerializerService().getObjectMapper().writeValueAsString(bindMounts); - } catch (JsonProcessingException e) { - log.error("Unable to serialize bind mounts.", e); - throw new RuntimeException(e); - } - } - - public List bindMountPojo() { - try { - CollectionType javaType = XDAT.getSerializerService().getObjectMapper().getTypeFactory() - .constructCollectionType(List.class, BindMount.class); - - return XDAT.getSerializerService().getObjectMapper().readValue(bindMountsJson, javaType); - } catch (IOException e) { - log.error("Unable to deserialize bind mounts.", e); - throw new RuntimeException(e); - } - } - - public static String containerSpecPojo(ContainerSpec containerSpec) { - try { - return XDAT.getSerializerService().getObjectMapper().writeValueAsString(containerSpec); - } catch (JsonProcessingException e) { - log.error("Unable to serialize containerSpec.", e); - throw new RuntimeException(e); - } - } - - public ContainerSpec containerSpecPojo() { - try { - return XDAT.getSerializerService().getObjectMapper().readValue(containerSpecJson, ContainerSpec.class); - } catch (IOException e) { - log.error("Unable to deserialize containerSpec.", e); - throw new RuntimeException(e); - } - } - - public PlacementSpec placementSpecPojo() { - try { - return XDAT.getSerializerService().getObjectMapper().readValue(placementSpecJson, PlacementSpec.class); - } catch (IOException e) { - log.error("Unable to deserialize containerSpec.", e); - throw new RuntimeException(e); - } - } - - - public static String placementSpecPojo(PlacementSpec placementSpec) { - try { - return XDAT.getSerializerService().getObjectMapper().writeValueAsString(placementSpec); - } catch (JsonProcessingException e) { - log.error("Unable to serialize placementSpec.", e); - throw new RuntimeException(e); - } - } - - - public ResourceSpec resourceSpecPojo() { - try { - return XDAT.getSerializerService().getObjectMapper().readValue(resourceSpecJson, ResourceSpec.class); - } catch (IOException e) { - log.error("Unable to deserialize resourceSpecJson.", e); - throw new RuntimeException(e); - } - } - - - public static String resourceSpecPojo(ResourceSpec resourceSpec) { - try { - return XDAT.getSerializerService().getObjectMapper().writeValueAsString(resourceSpec); - } catch (JsonProcessingException e) { - log.error("Unable to serialize resourceSpecJson.", e); - throw new RuntimeException(e); - } - } - public XnatUserOptions toPojo() { return XnatUserOptions.builder() .userId(userId) @@ -221,11 +102,7 @@ public XnatUserOptions toPojo() { .itemId(itemId) .projectId(projectId) .eventTrackingId(eventTrackingId) - .environmentVariables(environmentVariables) - .bindMounts(bindMountPojo()) - .containerSpec(containerSpecPojo()) - .placementSpec(placementSpecPojo()) - .resourceSpec(resourceSpecPojo()) + .taskTemplate(taskTemplate) .build(); } @@ -236,10 +113,6 @@ public void update(final UserOptionsEntity update) { this.setItemId(update.getItemId()); this.setProjectId(update.getProjectId()); this.setEventTrackingId(update.getEventTrackingId()); - this.setEnvironmentVariables(update.getEnvironmentVariables()); - this.setBindMountsJson(update.getBindMountsJson()); - this.setContainerSpecJson(update.getContainerSpecJson()); - this.setPlacementSpecJson(update.getPlacementSpecJson()); - this.setResourceSpecJson(update.getResourceSpecJson()); + this.setTaskTemplate(update.getTaskTemplate()); } } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java new file mode 100644 index 0000000..853c95a --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java @@ -0,0 +1,95 @@ +package org.nrg.xnatx.plugins.jupyterhub.initialize; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.xapi.exceptions.DataFormatException; +import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; +import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnatx.plugins.jupyterhub.models.Profile; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.ContainerSpec; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.Placement; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.Resources; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Collections; + +@Component +@Slf4j +public class JupyterHubProfileInitializer extends AbstractInitializingTask { + + private final ProfileService profileService; + private final XFTManagerHelper xfmManagerHelper; + + @Autowired + public JupyterHubProfileInitializer(final ProfileService profileService, + final XFTManagerHelper xfmManagerHelper) { + this.profileService = profileService; + this.xfmManagerHelper = xfmManagerHelper; + } + + @Override + public String getTaskName() { + return "JupyterHubProfileInitializer"; + } + + @Override + protected void callImpl() throws InitializingTaskException { + log.debug("Initializing JupyterHub profile."); + + if (!xfmManagerHelper.isInitialized()) { + log.debug("XFT not initialized, deferring execution."); + throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); + } + + if (profileService.getAll().size() > 0) { + log.debug("JupyterHub profile already exists. Skipping initialization."); + return; + } + + Profile profile = buildDefaultProfile(); + + try { + profileService.create(profile); + log.info("Created default JupyterHub profile."); + } catch (DataFormatException e) { + log.error("Error creating default JupyterHub profile.", e); + } + } + + private Profile buildDefaultProfile() { + ContainerSpec containerSpec = ContainerSpec.builder() + .image("jupyterhub/datascience-notebook:hub-3.0.0") + .mounts(Collections.emptyList()) + .env(Collections.emptyMap()) + .labels(Collections.emptyMap()) + .build(); + + Placement placement = Placement.builder() + .constraints(Collections.emptyList()) + .build(); + + Resources resources = Resources.builder() + .cpuLimit(null) + .cpuReservation(null) + .memLimit(null) + .memReservation(null) + .genericResources(Collections.emptyMap()) + .build(); + + TaskTemplate taskTemplate = TaskTemplate.builder() + .containerSpec(containerSpec) + .placement(placement) + .resources(resources) + .build(); + + return Profile.builder() + .name("Default") + .description("Default JupyterHub profile.") + .taskTemplate(taskTemplate) + .build(); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java index 147edc8..42f5b81 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java @@ -60,7 +60,7 @@ protected void callImpl() throws InitializingTaskException { final UserI jupyterhubUser = userManagementService.createUser(); jupyterhubUser.setLogin("jupyterhub"); - jupyterhubUser.setEmail("harmitage@miskatonic.edu"); + jupyterhubUser.setEmail("jupyterhub@jupyterhub.jupyterhub"); jupyterhubUser.setFirstname("jupyterhub"); jupyterhubUser.setLastname("jupyterhub"); jupyterhubUser.setPassword("jupyterhub"); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/BindMount.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/BindMount.java deleted file mode 100644 index 6ef3308..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/BindMount.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.models; - -import io.swagger.annotations.ApiModel; -import lombok.*; -import lombok.extern.slf4j.Slf4j; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -@EqualsAndHashCode(callSuper = false) -@Slf4j -@ApiModel(value = "Bind Mount", - description = "Describes the bind mounts used by the single-user Jupyter notebook server container.") -public class BindMount { - - private String name; - private boolean writable; - private String containerHostPath; - private String xnatHostPath; - private String jupyterHostPath; - -} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ContainerSpec.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ContainerSpec.java deleted file mode 100644 index c7c615e..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ContainerSpec.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.models; - -import lombok.*; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -@EqualsAndHashCode(callSuper = false) -@Slf4j -public class ContainerSpec { - - private String image; - private Map env; - private Map labels; - -} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DockerImage.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DockerImage.java index e29a14a..ca7159c 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DockerImage.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DockerImage.java @@ -9,6 +9,7 @@ @NoArgsConstructor @EqualsAndHashCode(callSuper = false) @Slf4j +@Deprecated public class DockerImage { private String image; diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/PlacementSpec.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/PlacementSpec.java deleted file mode 100644 index ce21b0d..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/PlacementSpec.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.models; - -import lombok.*; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -@EqualsAndHashCode(callSuper = false) -@Slf4j -public class PlacementSpec { - - private List constraints; - -} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Profile.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Profile.java new file mode 100644 index 0000000..85e8c69 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Profile.java @@ -0,0 +1,31 @@ +package org.nrg.xnatx.plugins.jupyterhub.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.ALWAYS; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Slf4j +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(ALWAYS) +public class Profile { + public static final String SWARM_SPAWNER = "dockerspawner.SwarmSpawner"; + public static final String KUBE_SPAWNER = "NOT_IMPLEMENTED"; + + private Long id; + private String name; + private String description; + private String spawner; + private Boolean enabled; + @JsonProperty("task_template") private TaskTemplate taskTemplate; + +} \ No newline at end of file diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/XnatUserOptions.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/XnatUserOptions.java index f1727d1..5372aa7 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/XnatUserOptions.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/XnatUserOptions.java @@ -5,9 +5,7 @@ import lombok.*; import lombok.extern.slf4j.Slf4j; import org.nrg.xnatx.plugins.jupyterhub.client.models.UserOptions; - -import java.util.List; -import java.util.Map; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; @Data @Builder @@ -26,11 +24,6 @@ public class XnatUserOptions implements UserOptions { private String projectId; private String eventTrackingId; - @JsonProperty("environment-variables") private Map environmentVariables; // Extra environment variables to set for the single-user server’s process. - @JsonProperty("mounts") private List bindMounts; - - @JsonProperty("container-spec") private ContainerSpec containerSpec; - @JsonProperty("placement-spec") private PlacementSpec placementSpec; - @JsonProperty("resource-spec") private ResourceSpec resourceSpec; + @JsonProperty("task_template") private TaskTemplate taskTemplate; } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/ContainerSpec.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/ContainerSpec.java new file mode 100644 index 0000000..2549f61 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/ContainerSpec.java @@ -0,0 +1,28 @@ +package org.nrg.xnatx.plugins.jupyterhub.models.docker; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.ALWAYS; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Slf4j +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(ALWAYS) +public class ContainerSpec { + + private String image; + private Map env; + private Map labels; + private List mounts; + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/Mount.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/Mount.java new file mode 100644 index 0000000..d9f36c7 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/Mount.java @@ -0,0 +1,28 @@ +package org.nrg.xnatx.plugins.jupyterhub.models.docker; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.ALWAYS; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Slf4j +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(ALWAYS) +public class Mount { + + private String target; + private String source; + private String type; + + @JsonProperty("read_only") + private boolean readOnly; + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/Placement.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/Placement.java new file mode 100644 index 0000000..99c8ff6 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/Placement.java @@ -0,0 +1,24 @@ +package org.nrg.xnatx.plugins.jupyterhub.models.docker; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.ALWAYS; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Slf4j +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(ALWAYS) +public class Placement { + + private List constraints; + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ResourceSpec.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/Resources.java similarity index 50% rename from src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ResourceSpec.java rename to src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/Resources.java index fbb0a55..4811653 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ResourceSpec.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/Resources.java @@ -1,20 +1,29 @@ -package org.nrg.xnatx.plugins.jupyterhub.models; +package org.nrg.xnatx.plugins.jupyterhub.models.docker; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.*; import lombok.extern.slf4j.Slf4j; +import java.util.Map; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.ALWAYS; + @Data @Builder @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode(callSuper = false) @Slf4j -public class ResourceSpec { +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(ALWAYS) +public class Resources { @JsonProperty("cpu_limit") private Double cpuLimit; @JsonProperty("cpu_reservation") private Double cpuReservation; @JsonProperty("mem_limit") private String memLimit; @JsonProperty("mem_reservation") private String memReservation; + @JsonProperty("generic_resources") private Map genericResources; } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/TaskTemplate.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/TaskTemplate.java new file mode 100644 index 0000000..d50f972 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/TaskTemplate.java @@ -0,0 +1,25 @@ +package org.nrg.xnatx.plugins.jupyterhub.models.docker; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.ALWAYS; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Slf4j +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(ALWAYS) +public class TaskTemplate { + + @JsonProperty("container_spec") private ContainerSpec containerSpec; + private Resources resources; + private Placement placement; + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java index 4949c91..636c684 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java @@ -63,6 +63,7 @@ public void setJupyterHubToken(final String jupyterHubToken) { } } + @Deprecated @NrgPreference(defaultValue = "[\n " + "{\"image\": \"jupyter/scipy-notebook:hub-3.0.0\", \"enabled\": \"true\"}\n " + "]", @@ -71,6 +72,7 @@ public List getDockerImages() { return getListValue(DOCKER_IMAGES_PREF_ID); } + @Deprecated public void setDockerImages(final List dockerImages) { try { setListValue(DOCKER_IMAGES_PREF_ID, dockerImages); @@ -170,11 +172,13 @@ public void setWorkspacePath(final String workspacePath) { } } + @Deprecated @NrgPreference(defaultValue = "") public Map getContainerSpecLabels() { return getMapValue(CONTAINER_SPEC_LABELS_PREF_ID); } + @Deprecated public void setContainerSpecLabels(final Map containerSpecLabels) { try { setMapValue(CONTAINER_SPEC_LABELS_PREF_ID, containerSpecLabels); @@ -183,11 +187,13 @@ public void setContainerSpecLabels(final Map containerSpecLabels } } + @Deprecated @NrgPreference(defaultValue = "[]") public List getPlacementSpecConstraints() { return getListValue(PLACEMENT_SPEC_CONSTRAINTS_PREF_ID); } + @Deprecated public void setPlacementSpecConstraints(List placementSpecConstraints) { try { setListValue(PLACEMENT_SPEC_CONSTRAINTS_PREF_ID, placementSpecConstraints); @@ -196,11 +202,13 @@ public void setPlacementSpecConstraints(List placementSpecConstraints) { } } + @Deprecated @NrgPreference(defaultValue = "0") public Double getResourceSpecCpuLimit() { return getDoubleValue(RESOURCE_SPEC_CPU_LIMIT_PREF_ID); } + @Deprecated public void setResourceSpecCpuLimit(final Double resourceSpecCpuLimit) { try { setDoubleValue(resourceSpecCpuLimit, RESOURCE_SPEC_CPU_LIMIT_PREF_ID); @@ -209,11 +217,13 @@ public void setResourceSpecCpuLimit(final Double resourceSpecCpuLimit) { } } + @Deprecated @NrgPreference(defaultValue = "0") public Double getResourceSpecCpuReservation() { return getDoubleValue(RESOURCE_SPEC_CPU_RESERVATION_PREF_ID); } + @Deprecated public void setResourceSpecCpuReservation(final Double resourceSpecCpuReservation) { try { setDoubleValue(resourceSpecCpuReservation, RESOURCE_SPEC_CPU_RESERVATION_PREF_ID); @@ -222,11 +232,13 @@ public void setResourceSpecCpuReservation(final Double resourceSpecCpuReservatio } } + @Deprecated @NrgPreference(defaultValue = "") public String getResourceSpecMemLimit() { return getValue(RESOURCE_SPEC_MEM_LIMIT_PREF_ID); } + @Deprecated public void setResourceSpecMemLimit(final String resourceSpecMemLimit) { try { set(resourceSpecMemLimit, RESOURCE_SPEC_MEM_LIMIT_PREF_ID); @@ -235,11 +247,13 @@ public void setResourceSpecMemLimit(final String resourceSpecMemLimit) { } } + @Deprecated @NrgPreference(defaultValue = "") public String getResourceSpecMemReservation() { return getValue(RESOURCE_SPEC_MEM_RESERVATION_PREF_ID); } + @Deprecated public void setResourceSpecMemReservation(final String resourceSpecMemReservation) { try { set(resourceSpecMemReservation, RESOURCE_SPEC_MEM_RESERVATION_PREF_ID); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/ProfileDao.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/ProfileDao.java new file mode 100644 index 0000000..e444c35 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/ProfileDao.java @@ -0,0 +1,12 @@ +package org.nrg.xnatx.plugins.jupyterhub.repositories; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; +import org.nrg.xnatx.plugins.jupyterhub.entities.ProfileEntity; +import org.springframework.stereotype.Repository; + +@Repository +@Slf4j +public class ProfileDao extends AbstractHibernateDAO { + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java index 4258008..63bad23 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java @@ -144,8 +144,8 @@ public void startServer(@ApiParam(value = "username", required = true) @PathVari @ApiParam(value = "itemLabel", required = true) @RequestParam("itemLabel") final String itemLabel, @ApiParam(value = "projectId", required = false) @RequestParam(value = "projectId", required = false) final String projectId, @ApiParam(value = "eventTrackingId", required = true) @RequestParam(value = "eventTrackingId") final String eventTrackingId, - @ApiParam(value = "dockerImage", required = true) @RequestParam(value = "dockerImage") final String dockerImage) throws UserNotFoundException, UserInitException { - jupyterHubService.startServer(getUserI(username), xsiType, itemId, itemLabel, projectId, eventTrackingId, dockerImage); + @ApiParam(value = "profileId", required = true) @RequestParam(value = "profileId") final Long profileId) throws UserNotFoundException, UserInitException { + jupyterHubService.startServer(getUserI(username), xsiType, itemId, itemLabel, projectId, eventTrackingId, profileId); } @ApiOperation(value = "Starts a Jupyter server for the user", @@ -164,8 +164,8 @@ public void startNamedServer(@ApiParam(value = "username", required = true) @Pat @ApiParam(value = "itemLabel", required = true) @RequestParam("itemLabel") final String itemLabel, @ApiParam(value = "projectId", required = false) @RequestParam(value = "projectId", required = false) final String projectId, @ApiParam(value = "eventTrackingId", required = true) @RequestParam(value = "eventTrackingId") final String eventTrackingId, - @ApiParam(value = "dockerImage", required = true) @RequestParam(value = "dockerImage") final String dockerImage) throws UserNotFoundException, UserInitException { - jupyterHubService.startServer(getUserI(username), servername, xsiType, itemId, itemLabel, projectId, eventTrackingId, dockerImage); + @ApiParam(value = "profileId", required = true) @RequestParam(value = "profileId") final Long profileId) throws UserNotFoundException, UserInitException { + jupyterHubService.startServer(getUserI(username), servername, xsiType, itemId, itemLabel, projectId, eventTrackingId, profileId); } @ApiOperation(value = "Returns the last known user options for the default server", response = XnatUserOptions.class) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java index 07ce67d..7fba89b 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java @@ -111,34 +111,6 @@ public Map getSpecifiedPreference(@ApiParam(value = "The Jupyter // I think there is a bug in the preference library with caching hence the switch. switch (preference) { - case (JupyterHubPreferences.DOCKER_IMAGES_PREF_ID): { - value = jupyterHubPreferences.getDockerImages(); - break; - } - case (JupyterHubPreferences.CONTAINER_SPEC_LABELS_PREF_ID): { - value = jupyterHubPreferences.getContainerSpecLabels(); - break; - } - case (JupyterHubPreferences.PLACEMENT_SPEC_CONSTRAINTS_PREF_ID): { - value = jupyterHubPreferences.getPlacementSpecConstraints(); - break; - } - case (JupyterHubPreferences.RESOURCE_SPEC_CPU_LIMIT_PREF_ID): { - value = jupyterHubPreferences.getResourceSpecCpuLimit(); - break; - } - case (JupyterHubPreferences.RESOURCE_SPEC_CPU_RESERVATION_PREF_ID): { - value = jupyterHubPreferences.getResourceSpecCpuReservation(); - break; - } - case (JupyterHubPreferences.RESOURCE_SPEC_MEM_LIMIT_PREF_ID): { - value = jupyterHubPreferences.getResourceSpecMemLimit(); - break; - } - case (JupyterHubPreferences.RESOURCE_SPEC_MEM_RESERVATION_PREF_ID): { - value = jupyterHubPreferences.getResourceSpecMemReservation(); - break; - } case (JupyterHubPreferences.INACTIVITY_TIMEOUT_PREF_ID): { value = jupyterHubPreferences.getInactivityTimeout(); break; diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApi.java new file mode 100644 index 0000000..a86cb33 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApi.java @@ -0,0 +1,120 @@ +package org.nrg.xnatx.plugins.jupyterhub.rest; + +import io.swagger.annotations.*; +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.annotations.XapiRestController; +import org.nrg.xapi.exceptions.DataFormatException; +import org.nrg.xapi.exceptions.NotFoundException; +import org.nrg.xapi.rest.AbstractXapiRestController; +import org.nrg.xapi.rest.XapiRequestMapping; +import org.nrg.xdat.security.helpers.AccessLevel; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnatx.plugins.jupyterhub.models.Profile; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.util.Comparator; +import java.util.List; + +import static java.util.stream.Collectors.toList; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.web.bind.annotation.RequestMethod.*; + + +@Api("JupyterHub Profiles API") +@XapiRestController +@RequestMapping("/jupyterhub/profiles") +@Slf4j +public class JupyterHubProfilesApi extends AbstractXapiRestController { + + private final ProfileService profileService; + + @Autowired + public JupyterHubProfilesApi(final UserManagementServiceI userManagementService, + final RoleHolder roleHolder, + final ProfileService profileService) { + super(userManagementService, roleHolder); + this.profileService = profileService; + } + + @ApiOperation(value = "Get a profiles.", response = Profile.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Profile successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = AccessLevel.Authenticated) + public Profile get(@ApiParam(value = "id", required = true) @PathVariable final Long id) throws NotFoundException { + return profileService.get(id).orElseThrow(() -> new NotFoundException("No profile with id " + id + " exists.")); + } + + @ApiOperation(value = "Get all profiles.", response = Profile.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Profiles successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = AccessLevel.Authenticated) + public List getAll() { + return profileService.getAll() + .stream() + .sorted(Comparator.comparing(Profile::getName)).collect(toList()); + } + + @ApiOperation(value = "Create a profile.") + @ApiResponses({ + @ApiResponse(code = 201, message = "Profile successfully created."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.CREATED) + @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = POST, restrictTo = AccessLevel.Admin) + public Long create(@ApiParam(value = "profile", required = true) @RequestBody final Profile profile) throws DataFormatException { + return profileService.create(profile); + } + + @ApiOperation(value = "Update a profile.") + @ApiResponses({ + @ApiResponse(code = 200, message = "Profile successfully updated."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = PUT, restrictTo = AccessLevel.Admin) + public void update(@ApiParam(value = "id", required = true) @PathVariable final Long id, + @ApiParam(value = "profile", required = true) @RequestBody final Profile profile) throws NotFoundException, DataFormatException { + if (!id.equals(profile.getId())) { + throw new DataFormatException("The profile ID in the path must match the ID in the body."); + } else if (!profileService.exists(id)) { + throw new NotFoundException("No profile with id " + id + " exists."); + } + + profileService.update(profile); + } + + @ApiOperation(value = "Delete a profile.") + @ApiResponses({ + @ApiResponse(code = 200, message = "Profile successfully deleted."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = DELETE, restrictTo = AccessLevel.Admin) + public void delete(@ApiParam(value = "id", required = true) @PathVariable final Long id) throws NotFoundException { + if (!profileService.exists(id)) { + throw new NotFoundException("No profile with id " + id + " exists."); + } + + profileService.delete(id); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java index 68f4e30..51e299c 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java @@ -18,8 +18,8 @@ public interface JupyterHubService { List getUsers(); Optional getServer(UserI user); Optional getServer(UserI user, String servername); - void startServer(UserI user, String xsiType, String itemId, String itemLabel, String projectId, String eventTrackingId, String dockerImage); - void startServer(UserI user, String servername, String xsiType, String itemId, String itemLabel, String projectId, String eventTrackingId, String dockerImage); + void startServer(UserI user, String xsiType, String itemId, String itemLabel, String projectId, String eventTrackingId, Long profileId); + void startServer(UserI user, String servername, String xsiType, String itemId, String itemLabel, String projectId, String eventTrackingId, Long profileId); void stopServer(UserI user, String eventTrackingId); void stopServer(UserI user, String servername, String eventTrackingId); Token createToken(UserI user, String note, Integer expiresIn); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileEntityService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileEntityService.java new file mode 100644 index 0000000..b4ed6d9 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileEntityService.java @@ -0,0 +1,8 @@ +package org.nrg.xnatx.plugins.jupyterhub.services; + +import org.nrg.framework.orm.hibernate.BaseHibernateService; +import org.nrg.xnatx.plugins.jupyterhub.entities.ProfileEntity; + +public interface ProfileEntityService extends BaseHibernateService { + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileService.java new file mode 100644 index 0000000..96218ae --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileService.java @@ -0,0 +1,18 @@ +package org.nrg.xnatx.plugins.jupyterhub.services; + +import org.nrg.xapi.exceptions.DataFormatException; +import org.nrg.xnatx.plugins.jupyterhub.models.Profile; + +import java.util.List; +import java.util.Optional; + +public interface ProfileService { + + boolean exists(Long id); + Long create(Profile profile) throws DataFormatException; + Optional get(Long id); + List getAll(); + void update(Profile profile) throws DataFormatException; + void delete(Long id); + +} \ No newline at end of file diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java index 9c9d1f3..110af9f 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java @@ -20,6 +20,6 @@ public interface UserOptionsService { Optional retrieveUserOptions(UserI user); Optional retrieveUserOptions(UserI user, String servername); - void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, String dockerImage, String eventTrackingId); + void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, Long profileId, String eventTrackingId); } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java index 14a0f1a..aa44607 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java @@ -17,6 +17,7 @@ import org.nrg.xnatx.plugins.jupyterhub.models.XnatUserOptions; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; import org.nrg.xnatx.plugins.jupyterhub.services.JupyterHubService; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; import org.springframework.beans.factory.annotation.Autowired; @@ -43,6 +44,7 @@ public class DefaultJupyterHubService implements JupyterHubService { private final UserOptionsService userOptionsService; private final JupyterHubPreferences jupyterHubPreferences; private final UserManagementServiceI userManagementService; + private final ProfileService profileService; @Autowired public DefaultJupyterHubService(final JupyterHubClient jupyterHubClient, @@ -50,13 +52,15 @@ public DefaultJupyterHubService(final JupyterHubClient jupyterHubClient, final PermissionsHelper permissionsHelper, final UserOptionsService userOptionsService, final JupyterHubPreferences jupyterHubPreferences, - final UserManagementServiceI userManagementService) { + final UserManagementServiceI userManagementService, + final ProfileService profileService) { this.jupyterHubClient = jupyterHubClient; this.eventService = eventService; this.permissionsHelper = permissionsHelper; this.userOptionsService = userOptionsService; this.jupyterHubPreferences = jupyterHubPreferences; this.userManagementService = userManagementService; + this.profileService = profileService; } /** @@ -139,20 +143,20 @@ public Optional getServer(final UserI user, final String servername) { * mounted to the Jupyter notebook server container. The progress of the server launch is tracked with the event * tracking api. * - * @param user User to start the server for. - * @param xsiType Accepts xnat:projectData, xnat:subjectData, xnat:experimentData and its children, and - * xdat:stored_search - * @param itemId The provided id's resources will be mounted to the Jupyter notebook server container - * @param itemLabel The label of the provided item (e.g. the subject label or experiment label). - * @param projectId Can be null for xdat:stored_search - * @param eventTrackingId Use with the event tracking api to track progress. - * @param dockerImage The docker image to use for the single-user notebook server container + * @param user User to start the server for. + * @param xsiType Accepts xnat:projectData, xnat:subjectData, xnat:experimentData and its children, and + * xdat:stored_search + * @param itemId The provided id's resources will be mounted to the Jupyter notebook server container + * @param itemLabel The label of the provided item (e.g. the subject label or experiment label). + * @param projectId Can be null for xdat:stored_search + * @param eventTrackingId Use with the event tracking api to track progress. + * @param profileId The id of the profile to use for this server. */ @Override public void startServer(final UserI user, final String xsiType, final String itemId, final String itemLabel, @Nullable final String projectId, final String eventTrackingId, - final String dockerImage) { - startServer(user, "", xsiType, itemId, itemLabel, projectId, eventTrackingId, dockerImage); + final Long profileId) { + startServer(user, "", xsiType, itemId, itemLabel, projectId, eventTrackingId, profileId); } /** @@ -160,20 +164,20 @@ public void startServer(final UserI user, final String xsiType, final String ite * mounted to the Jupyter notebook server container. The progress of the server launch is tracked with the event * tracking api. * - * @param user User to start the server for. - * @param servername The name of the server that will be started - * @param xsiType Accepts xnat:projectData, xnat:subjectData, xnat:experimentData and its children, and - * xdat:stored_search - * @param itemId The provided id's resources will be mounted to the Jupyter notebook server container - * @param itemLabel The label of the provided item (e.g. the subject label or experiment label). - * @param projectId Can be null for xdat:stored_search - * @param eventTrackingId Use with the event tracking api to track progress. - * @param dockerImage The docker image to use for the single-user notebook server container + * @param user User to start the server for. + * @param servername The name of the server that will be started + * @param xsiType Accepts xnat:projectData, xnat:subjectData, xnat:experimentData and its children, and + * xdat:stored_search + * @param itemId The provided id's resources will be mounted to the Jupyter notebook server container + * @param itemLabel The label of the provided item (e.g. the subject label or experiment label). + * @param projectId Can be null for xdat:stored_search + * @param eventTrackingId Use with the event tracking api to track progress. + * @param profileId The id of the profile to use for this server. */ @Override public void startServer(final UserI user, String servername, final String xsiType, final String itemId, final String itemLabel, @Nullable final String projectId, final String eventTrackingId, - final String dockerImage) { + final Long profileId) { eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, 0, "Starting Jupyter notebook server for user " + user.getUsername())); @@ -185,6 +189,13 @@ public void startServer(final UserI user, String servername, final String xsiTyp return; } + if (!profileService.exists(profileId)) { + eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, + JupyterServerEventI.Operation.Start, + "Failed to launch Jupyter notebook server. Profile does not exist.")); + return; + } + CompletableFuture.runAsync(() -> { // Check if JupyterHub is online try { @@ -219,7 +230,7 @@ public void startServer(final UserI user, String servername, final String xsiTyp JupyterServerEventI.Operation.Start, 20, "Building notebook server container configuration.")); - userOptionsService.storeUserOptions(user, servername, xsiType, itemId, projectId, dockerImage, eventTrackingId); + userOptionsService.storeUserOptions(user, servername, xsiType, itemId, projectId, profileId, eventTrackingId); eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, 30, diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileService.java new file mode 100644 index 0000000..8a2b1db --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileService.java @@ -0,0 +1,162 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.xapi.exceptions.DataFormatException; +import org.nrg.xnatx.plugins.jupyterhub.entities.ProfileEntity; +import org.nrg.xnatx.plugins.jupyterhub.models.Profile; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Default implementation of the profile service. Valid profiles must have a name, description, task template, container + * spec, and image defined. + */ +@Service +@Slf4j +public class DefaultProfileService implements ProfileService { + + private final ProfileEntityService profileEntityService; + + @Autowired + public DefaultProfileService(final ProfileEntityService profileEntityService) { + this.profileEntityService = profileEntityService; + } + + /** + * Check if a profile exists. + * @param id profile id + * @return true if the profile exists, false otherwise + */ + @Override + public boolean exists(final Long id) { + final ProfileEntity profileEntity = profileEntityService.retrieve(id); + return profileEntity != null; + } + + /** + * Create a profile. + * @param profile profile to create + * @return id of the created profile + * + * @throws DataFormatException if the profile is invalid + */ + @Override + public Long create(final Profile profile) throws DataFormatException { + profile.setId(null); + + // Validate the profile + validate(profile); + + final ProfileEntity profileEntity = ProfileEntity.fromPojo(profile); + ProfileEntity created = profileEntityService.create(profileEntity); + created.getProfile().setId(created.getId()); // Update the id in the profile + profileEntityService.update(created); + return created.getId(); + } + + /** + * Get a profile. + * @param id profile id + * @return optional profile if it exists + */ + @Override + public Optional get(final Long id) { + Optional profileEntity = Optional.ofNullable(profileEntityService.retrieve(id)); + return profileEntity.map(ProfileEntity::toPojo); + } + + /** + * Get all profiles. + * @return list of profiles + */ + @Override + public List getAll() { + return profileEntityService.getAll() + .stream() + .map(ProfileEntity::toPojo) + .collect(Collectors.toList()); + } + + /** + * Update an existing profile. + * @param toUpdate profile to update + * + * @throws DataFormatException if the profile is invalid + */ + @Override + public void update(Profile toUpdate) throws DataFormatException { + final ProfileEntity profileEntity = profileEntityService.retrieve(toUpdate.getId()); + + // Profile not found + if (profileEntity == null) { + DataFormatException exception = new DataFormatException(); + exception.addInvalidField("id", "Profile not found"); + throw exception; + } + + // Validate the profile + validate(toUpdate); + + final ProfileEntity template = ProfileEntity.fromPojo(toUpdate); + final ProfileEntity updated = profileEntity.update(template); + + profileEntityService.update(updated); + } + + /** + * Delete a profile. + * @param id profile id + */ + @Override + public void delete(final Long id) { + profileEntityService.delete(id); + } + + /** + * Validate a profile. Profile is invalid if: + * - name is empty + * - description is empty + * - task template is empty + * - container spec is empty + * - image is empty + * + * @param profile profile to validate + * @throws DataFormatException if the profile is invalid + */ + protected void validate(final Profile profile) throws DataFormatException { + DataFormatException exception = new DataFormatException(); + + if (profile.getName() == null || profile.getName().isEmpty()) { + exception.addInvalidField('"' + "name" + '"' + " cannot be empty"); + } + + if (profile.getDescription() == null || profile.getDescription().isEmpty()) { + exception.addInvalidField('"' + "description" + '"' + " cannot be empty"); + } + + if (profile.getTaskTemplate() == null) { + exception.addInvalidField('"' + "taskTemplate" + '"' + " cannot be empty"); + throw exception; + } + + if (profile.getTaskTemplate().getContainerSpec() == null) { + exception.addInvalidField('"' + "containerSpec" + '"' + " cannot be empty"); + throw exception; + } + + if (profile.getTaskTemplate().getContainerSpec().getImage() == null || profile.getTaskTemplate().getContainerSpec().getImage().isEmpty()) { + exception.addInvalidField('"' + "image" + '"' + " cannot be empty"); + } + + if (exception.hasDataFormatErrors()) { + throw exception; + } + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java index f35d140..e0302bc 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java @@ -17,7 +17,9 @@ import org.nrg.xnat.exceptions.InvalidArchiveStructure; import org.nrg.xnatx.plugins.jupyterhub.entities.UserOptionsEntity; import org.nrg.xnatx.plugins.jupyterhub.models.*; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.*; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsEntityService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.services.UserWorkspaceService; @@ -43,7 +45,7 @@ public class DefaultUserOptionsService implements UserOptionsService { private final SiteConfigPreferences siteConfigPreferences; private final UserOptionsEntityService userOptionsEntityService; private final PermissionsHelper permissionsHelper; - + private final ProfileService profileService; @Autowired public DefaultUserOptionsService(final JupyterHubPreferences jupyterHubPreferences, @@ -52,7 +54,8 @@ public DefaultUserOptionsService(final JupyterHubPreferences jupyterHubPreferenc final AliasTokenService aliasTokenService, final SiteConfigPreferences siteConfigPreferences, final UserOptionsEntityService userOptionsEntityService, - final PermissionsHelper permissionsHelper) { + final PermissionsHelper permissionsHelper, + final ProfileService profileService) { this.jupyterHubPreferences = jupyterHubPreferences; this.userWorkspaceService = userWorkspaceService; this.searchHelperService = searchHelperService; @@ -60,6 +63,7 @@ public DefaultUserOptionsService(final JupyterHubPreferences jupyterHubPreferenc this.siteConfigPreferences = siteConfigPreferences; this.userOptionsEntityService = userOptionsEntityService; this.permissionsHelper = permissionsHelper; + this.profileService = profileService; } @Override @@ -266,7 +270,7 @@ public Optional retrieveUserOptions(UserI user, String serverna @Override public void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, - String dockerImage, String eventTrackingId) { + Long profileId, String eventTrackingId) { log.debug("Storing user options for user '{}' server '{}' xsiType '{}' id '{}' projectId '{}'", user.getUsername(), servername, xsiType, id, projectId); @@ -274,6 +278,13 @@ public void storeUserOptions(UserI user, String servername, String xsiType, Stri return; } + Optional profileOpt = profileService.get(profileId); + if (!profileOpt.isPresent()) { + return; + } + + Profile profile = profileOpt.get(); + // specific xsi type -> general xsi type if (instanceOf(xsiType, XnatExperimentdata.SCHEMA_ELEMENT_NAME)) { xsiType = XnatExperimentdata.SCHEMA_ELEMENT_NAME; @@ -308,51 +319,37 @@ public void storeUserOptions(UserI user, String servername, String xsiType, Stri // Gather mounts final Path workspacePath = userWorkspaceService.getUserWorkspace(user); - final BindMount workspaceMount = BindMount.builder() - .name("user-workspace") - .writable(true) - .xnatHostPath(workspacePath.toString()) - .containerHostPath(translatePath(workspacePath.toString())) - .jupyterHostPath(Paths.get("/workspace", user.getUsername()).toString()) + + // Add user workspace mount + final Mount userNotebookDirectoryMount = Mount.builder() + .source(translatePath(workspacePath.toString())) + .target(Paths.get("/workspace", user.getUsername()).toString()) + .type("bind") + .readOnly(false) .build(); - final List xnatDataMounts = paths.entrySet().stream() - .map((entry) -> BindMount.builder() - .name(entry.getKey()) - .writable(false) - .xnatHostPath(entry.getValue()) - .containerHostPath(translatePath(entry.getValue())) - .jupyterHostPath(Paths.get(entry.getKey()).toString()) + // Add xnat data mounts + final List xnatDataMounts = paths.entrySet().stream() + .map((entry) -> Mount.builder() + .source(translatePath(entry.getValue())) + .target(Paths.get(entry.getKey()).toString()) + .type("bind") + .readOnly(true) .build()) .collect(Collectors.toList()); - final List mounts = new ArrayList<>(Collections.emptyList()); - mounts.add(workspaceMount); + // Collect mounts + final List mounts = new ArrayList<>(Collections.emptyList()); + mounts.add(userNotebookDirectoryMount); mounts.addAll(xnatDataMounts); - // Get env variables - Map environmentVariables = getDefaultEnvironmentVariables(user, xsiType, id); + // Add mounts + TaskTemplate taskTemplate = profile.getTaskTemplate(); + taskTemplate.getContainerSpec().getMounts().addAll(mounts); - // ContainerSpec - Map containerSpecLabels = jupyterHubPreferences.getContainerSpecLabels(); - ContainerSpec containerSpec = ContainerSpec.builder() - .labels(containerSpecLabels) - .image(dockerImage) - .build(); - - // PlacementSpec - List placementSpecConstraints = jupyterHubPreferences.getPlacementSpecConstraints(); - PlacementSpec placementSpec = PlacementSpec.builder() - .constraints(placementSpecConstraints) - .build(); - - // ResourceSpec - ResourceSpec resourceSpec = ResourceSpec.builder() - .cpuLimit(jupyterHubPreferences.getResourceSpecCpuLimit()) - .cpuReservation(jupyterHubPreferences.getResourceSpecCpuReservation()) - .memLimit(jupyterHubPreferences.getResourceSpecMemLimit()) - .memReservation(jupyterHubPreferences.getResourceSpecMemReservation()) - .build(); + // Get and add env variables + Map environmentVariables = getDefaultEnvironmentVariables(user, xsiType, id); + taskTemplate.getContainerSpec().getEnv().putAll(environmentVariables); // Store the user options UserOptionsEntity userOptionsEntity = UserOptionsEntity.builder() @@ -362,11 +359,7 @@ public void storeUserOptions(UserI user, String servername, String xsiType, Stri .itemId(id) .projectId(projectId) .eventTrackingId(eventTrackingId) - .environmentVariables(environmentVariables) - .bindMountsJson(UserOptionsEntity.bindMountPojo(mounts)) - .containerSpecJson(UserOptionsEntity.containerSpecPojo(containerSpec)) - .placementSpecJson(UserOptionsEntity.placementSpecPojo(placementSpec)) - .resourceSpecJson(UserOptionsEntity.resourceSpecPojo(resourceSpec)) + .taskTemplate(taskTemplate) .build(); userOptionsEntityService.createOrUpdate(userOptionsEntity); @@ -395,6 +388,7 @@ protected Map getDefaultEnvironmentVariables(UserI user, String defaultEnvironmentVariables.put("XNAT_DATA", "/data"); defaultEnvironmentVariables.put("XNAT_XSI_TYPE", xsiType); defaultEnvironmentVariables.put("XNAT_ITEM_ID", id); + defaultEnvironmentVariables.put("JUPYTERHUB_ROOT_DIR", Paths.get("/workspace", user.getUsername()).toString()); return defaultEnvironmentVariables; } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateProfileEntityService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateProfileEntityService.java new file mode 100644 index 0000000..e33ad60 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateProfileEntityService.java @@ -0,0 +1,18 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + + +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; +import org.nrg.xnatx.plugins.jupyterhub.entities.ProfileEntity; +import org.nrg.xnatx.plugins.jupyterhub.repositories.ProfileDao; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileEntityService; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Slf4j +@Transactional +public class HibernateProfileEntityService extends AbstractHibernateEntityService implements ProfileEntityService { + +} diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-profiles.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-profiles.js new file mode 100644 index 0000000..e131169 --- /dev/null +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-profiles.js @@ -0,0 +1,645 @@ +// noinspection RegExpRedundantEscape + +/*! + * JupyterHub Profiles + */ + +console.debug('jupyterhub-profiles.js'); + +var XNAT = getObject(XNAT || {}); +XNAT.app = getObject(XNAT.app || {}); +XNAT.app.activityTab = getObject(XNAT.app.activityTab || {}); +XNAT.plugin = getObject(XNAT.plugin || {}); +XNAT.plugin.jupyterhub = getObject(XNAT.plugin.jupyterhub || {}); +XNAT.plugin.jupyterhub.profiles = getObject(XNAT.plugin.jupyterhub.profiles || {}); + +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + return factory(); + } +}(function () { + + XNAT.plugin.jupyterhub.profiles.empty = () => { + return { + "enabled": true, + "id": null, + "name": "", + "description": "", + "projects": [], + "spawner": "dockerspawner.SwarmSpawner", + "task_template": { + "container_spec": { + "env": {}, + "image": "", + "mounts": [] + }, + "placement": { + "constraints": [] + }, + "resources": { + "cpu_limit": null, + "cpu_reservation": null, + "mem_limit": null, + "mem_reservation": null, + "generic_resources": {} + } + } + } + }; + + XNAT.plugin.jupyterhub.profiles.get = async (id) => { + console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.get(${id})`); + + const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles/${id}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error('Error fetching profile'); + } + } + + XNAT.plugin.jupyterhub.profiles.getAll = async () => { + console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.getAll()`); + + const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error('Error fetching profiles'); + } + } + + XNAT.plugin.jupyterhub.profiles.create = async (profile) => { + console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.create`); + + const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles`); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(profile) + }); + + if (response.ok) { + return response.text(); + } else { + throw new Error('Error creating profile'); + } + } + + XNAT.plugin.jupyterhub.profiles.update = async (profile) => { + console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.update`); + + const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles/${profile.id}`); + + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(profile) + }); + + if (!response.ok) { + throw new Error('Error updating profile'); + } + } + + XNAT.plugin.jupyterhub.profiles.delete = async (id) => { + console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.delete(${id})`); + + const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles/${id}`); + + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Error deleting profile'); + } + } + + XNAT.plugin.jupyterhub.profiles.getForProject = async (projectId) => { + console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.getForProject(${projectId})`); + + const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles/projects/${projectId}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error('Error getting profiles for project'); + } + } + + XNAT.plugin.jupyterhub.profiles.manager = async (containerId = 'jupyterhub-profiles') => { + console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.manager`); + + const containerEl = document.getElementById(containerId); + const footerEl = containerEl.parentElement.parentElement.querySelector('.panel-footer'); + + const clearContainer = () => containerEl.innerHTML = ''; + + const editButton = (profile) => { + return spawn('button.btn.sm', { + title: 'Edit', + onclick: function (e) { + e.preventDefault(); + editor(profile); + } + }, [spawn('span.fa.fa-pencil')]); + } + + const deleteButton = (profile) => { + return spawn('button.btn.sm.delete', { + title: 'Delete', + onclick: function (e) { + e.preventDefault(); + xmodal.confirm({ + title: 'Delete Profile', + height: 200, + width: 600, + scroll: false, + content: "" + + "

    Are you sure you want to delete the profile " + profile.name + "?

    " + + "

    This action cannot be undone.

    ", + okAction: function () { + XNAT.plugin.jupyterhub.profiles.delete(profile.id) + .then(() => { + refreshTable(); + XNAT.ui.banner.top(2000, 'Profile deleted', 'success'); + }) + .catch((err) => { + console.error(err); + XNAT.ui.banner.top(2000, 'Error deleting profile', 'error'); + }); + } + }) + } + }, [spawn('i.fa.fa-trash')]); + } + + const enabledToggle = (profile) => { + let enabled = profile['enabled']; + let ckbox = spawn('input', { + type: 'checkbox', + checked: enabled, + value: enabled ? 'true' : 'false', + data: {checked: enabled}, + onchange: () => { + profile['enabled'] = !enabled; + + XNAT.plugin.jupyterhub.profiles.update(profile).then(() => { + XNAT.ui.banner.top(2000, 'Profile ' + (enabled ? 'disabled' : 'enabled'), 'success'); + }).catch((err) => { + console.error(err); + XNAT.ui.banner.top(2000, 'Error updating profile', 'error'); + }); + } + }); + + return spawn('label.switchbox', [ckbox, ['span.switchbox-outer', [['span.switchbox-inner']]]]); + } + + const spacer = (width) => { + return spawn('i.spacer', { + style: { + display: 'inline-block', + width: width + 'px' + } + }) + } + + const refreshTable = async () => { + const profiles = await XNAT.plugin.jupyterhub.profiles.getAll(); + + if (profiles.length === 0) { + clearContainer(); + containerEl.innerHTML = '

    No profiles found. Click the "New Profile" button to create one.

    '; + return; + } + + const profilesTable = XNAT.ui.panel.dataTable({ + name: 'profile-table', + data: profiles, + items: { + id: '~!', + name: { + label: ' Profile Name', + sort: true + }, + image: { + label: 'Docker Image', + apply: function () { + return spawn('div.center', this['task_template']['container_spec']['image']) + } + }, + cpu: { + label: 'CPU Res / Lim', + apply: function () { + let cpuReservation = + this.task_template && + this.task_template.resources && + this.task_template.resources.cpu_reservation || '-'; + let cpuLimit = + this.task_template && + this.task_template.resources && + this.task_template.resources.cpu_limit || '-'; + let cpu = cpuReservation + ' / ' + cpuLimit; + return spawn('div.center', cpu); + } + }, + memory: { + label: 'Memory Res / Lim', + apply: function () { + let memReservation = + this.task_template && + this.task_template.resources && + this.task_template.resources.mem_reservation || '-'; + let memLimit = + this.task_template && + this.task_template.resources && + this.task_template.resources.mem_limit || '-'; + let mem = memReservation + ' / ' + memLimit; + return spawn('div.center', mem); + } + }, + enables: { + label: 'Enabled', + apply: function () { + return spawn('div.center', [enabledToggle(this)]); + } + }, + actions: { + label: 'Actions', + apply: function () { + return spawn('div.center', [ + editButton(this), + spacer(4), + deleteButton(this) + ]); + } + } + }, + }) + + // Clear the container and add the table + clearContainer(); + containerEl.append(profilesTable.element); + const elementWrapper = profilesTable.element.querySelector('.element-wrapper') + if (elementWrapper) { + // Make the table full width, the default is too narrow + elementWrapper.style.width = '100%'; + } + } + + const editor = (profile) => { + const doWhat = profile ? 'Edit' : 'Create'; + const isNew = !profile; + profile = profile || XNAT.plugin.jupyterhub.profiles.empty(); + + const editorDialogId = 'editor-dialog'; + + XNAT.dialog.open({ + title: `${doWhat} JupyterHub Profile`, + content: spawn(`form#${editorDialogId}`), + maxBtn: true, + width: 600, + beforeShow: () => { + const formEl = document.getElementById(editorDialogId); + formEl.classList.add('panel'); + + let profileName = isNew ? '' : profile.name || ''; + let profileDescription = isNew ? '' : profile.description || ''; + let profileImage = isNew ? '' : + profile.task_template && + profile.task_template.container_spec && + profile.task_template.container_spec.image || ''; + let profileCpuReservation = isNew ? '' : + profile.task_template && + profile.task_template.resources && + profile.task_template.resources.cpu_reservation || ''; + let profileCpuLimit = isNew ? '' : + profile.task_template && + profile.task_template.resources && + profile.task_template.resources.cpu_limit || ''; + let profileMemoryReservation = isNew ? '' : + profile.task_template && + profile.task_template.resources && + profile.task_template.resources.mem_reservation || ''; + let profileMemoryLimit = isNew ? '' : + profile.task_template && + profile.task_template.resources && + profile.task_template.resources.mem_limit || ''; + let profilePlacementConstraints = isNew ? '' : + profile.task_template && + profile.task_template.placement && + profile.task_template.placement.constraints ? + profile.task_template.placement.constraints.join(',\r\n') : ''; + let profileGenericResources = isNew ? '' : + profile.task_template && + profile.task_template.resources && + profile.task_template.resources.generic_resources ? + Object.entries(profile.task_template.resources.generic_resources).map(([k, v]) => `${k}=${v}`).join(',\r\n') : ''; + let profileEnv = isNew ? '' : + profile.task_template && + profile.task_template.container_spec && + profile.task_template.container_spec.env ? + Object.entries(profile.task_template.container_spec.env).map(([k, v]) => `${k}=${v}`).join(',\r\n') : ''; + let profileMounts = isNew ? '' : + profile.task_template && + profile.task_template.container_spec && + profile.task_template.container_spec.mounts ? + profile.task_template.container_spec.mounts.map(m => `${m['source']}:${m['target']}:${m['read_only'] ? 'ro' : 'rw'}`).join(',\r\n') : ''; + + formEl.append(spawn('!', [ + XNAT.ui.panel.input.text({ + name: 'name', + id: 'name', + label: 'Name *', + description: 'The name of this profile. This will be displayed to users when they select this configuration.', + value: profileName, + }).element, + XNAT.ui.panel.input.textarea({ + name: 'description', + id: 'description', + label: 'Description *', + description: 'A description of this profile. This will be displayed to users when they select this configuration.', + value: profileDescription, + rows: 5, + }).element, + XNAT.ui.panel.input.text({ + name: 'image', + id: 'image', + label: 'Docker Image *', + description: 'The Docker image to use for this profile. This should be the full image name, including the tag. Example: jupyter/scipy-notebook:hub-version', + value: profileImage, + }).element, + XNAT.ui.panel.input.text({ + name: 'cpu-reservation', + id: 'cpu-reservation', + label: 'CPU Reservation', + description: 'The number of CPUs that this container will reserve. Fractional values (e.g. 0.5) are allowed.', + value: profileCpuReservation, + }).element, + XNAT.ui.panel.input.text({ + name: 'cpu-limit', + id: 'cpu-limit', + label: 'CPU Limit', + description: 'The maximum number of CPUs that this container can use. Fractional values (e.g. 3.5) are allowed.', + value: profileCpuLimit, + }).element, + XNAT.ui.panel.input.text({ + name: 'memory-reservation', + id: 'memory-reservation', + label: 'Memory Reservation', + description: 'The amount of memory that this container will reserve. Allows for suffixes like "2G" or "256M".', + value: profileMemoryReservation, + }).element, + XNAT.ui.panel.input.text({ + name: 'memory-limit', + id: 'memory-limit', + label: 'Memory Limit', + description: 'The maximum amount of memory that this container can use. Allows for suffixes like "4G" or "512M".', + value: profileMemoryLimit, + }).element, + XNAT.ui.panel.input.textarea({ + name: 'placement-constraints', + id: 'placement-constraints', + label: 'Placement Constraints', + description: 'Enter a list of placement constraints in the form of comma-separated strings. For example: node.role==worker, node.labels.type==gpu', + value: profilePlacementConstraints, + rows: 5, + }).element, + XNAT.ui.panel.input.textarea({ + name: 'generic-resources', + id: 'generic-resources', + label: 'Generic Resources', + description: 'Enter a list of generic resources in the form of comma-separated strings. For example: GPU=2, FPGA=1', + value: profileGenericResources, + rows: 5, + }).element, + XNAT.ui.panel.input.textarea({ + name: 'environment-variables', + id: 'environment-variables', + label: 'Environment Variables', + description: 'Enter a list of environment variables in the form of comma-separated strings. For example: MY_ENV_VAR=foo, ANOTHER_ENV_VAR=bar', + // join with newlines and carriage returns to make it easier to edit + value: profileEnv, + rows: 5, + }).element, + XNAT.ui.panel.input.textarea({ + name: 'mounts', + id: 'mounts', + label: 'Bind Mounts', + description: 'Enter a list of additional bind mounts in the form of comma-separated strings. For example: /tools:/tools:ro, /home:/home:rw', + // join with newlines and carriage returns to make it easier to edit + value: profileMounts, + rows: 5, + }).element, + ])) + }, + buttons: [ + { + label: 'Save', + isDefault: true, + close: false, + action: function () { + const formEl = document.getElementById(editorDialogId); + + const nameEl = formEl.querySelector('#name'); + const descriptionEl = formEl.querySelector('#description'); + const imageEl = formEl.querySelector('#image'); + const cpuLimitEl = formEl.querySelector('#cpu-limit'); + const cpuReservationEl = formEl.querySelector('#cpu-reservation'); + const memoryLimitEl = formEl.querySelector('#memory-limit'); + const memoryReservationEl = formEl.querySelector('#memory-reservation'); + const placementConstraintsEl = formEl.querySelector('#placement-constraints'); + const genericResourcesEl = formEl.querySelector('#generic-resources'); + const environmentVariablesEl = formEl.querySelector('#environment-variables'); + const mountsEl = formEl.querySelector('#mounts'); + + let validateNameEl = XNAT.validate(nameEl).reset().chain(); + validateNameEl.is('notEmpty').failure('Name is required'); + + let validateDescriptionEl = XNAT.validate(descriptionEl).reset().chain(); + validateDescriptionEl.is('notEmpty').failure('Description is required'); + + let validateImageEl = XNAT.validate(imageEl).reset().chain(); + validateImageEl.is('notEmpty').failure('Image is required'); + // must be in the format of image:tag + validateImageEl.is('regex', /^.+:.+$/).failure('Image must be in the format of image:tag'); + + let validateCpuLimitEl = XNAT.validate(cpuLimitEl).reset().chain(); + validateCpuLimitEl.is('allow-empty') + .is('decimal') + .is('greater-than', 0) + .failure('CPU Limit must be a number greater than 0 or be empty'); + + let validateCpuReservationEl = XNAT.validate(cpuReservationEl).reset().chain(); + validateCpuReservationEl.is('allow-empty') + .is('decimal') + .is('greater-than', 0) + .failure('CPU Reservation must be a number greater than 0 or be empty'); + + let validateMemoryLimitEl = XNAT.validate(memoryLimitEl).reset().chain(); + validateMemoryLimitEl.is('allow-empty') + .is('regex', /^$|^([1-9]+[0-9]*)+[KMGT]$/) // 512M, 2G, etc. + .failure('Memory Limit must be a number followed by a suffix of K, M, G, or T or be empty'); + + let validateMemoryReservationEl = XNAT.validate(memoryReservationEl).reset().chain(); + validateMemoryReservationEl.is('allow-empty') + .is('regex', /^$|^([1-9]+[0-9]*)+[KMGT]$/) // 512M, 2G, etc. + .failure('Memory Reservation must be a number followed by a suffix of K, M, G, or T or be empty'); + + let validatePlacementConstraintsEl = XNAT.validate(placementConstraintsEl).reset().chain(); + validatePlacementConstraintsEl.is('allow-empty') + .is('regex', /^$|^([A-Za-z0-9_\.]+(==|!=)[A-Za-z0-9_\.]+,?\s*)+$/) // node.role == worker, node.labels.type == gpu + .failure('Placement Constraints must be in the form of KEY==VALUE, KEY!=VALUE or be empty'); + + let validateGenericResourcesEl = XNAT.validate(genericResourcesEl).reset().chain(); + validateGenericResourcesEl.is('allow-empty') + .is('regex', /^$|^([A-Za-z_][A-Za-z0-9_]*=[^,]+,?\s*)+$/) + .failure('Generic Resources must be in the form of KEY=VALUE or be empty'); + + let validateEnvironmentVariablesEl = XNAT.validate(environmentVariablesEl).reset().chain(); + validateEnvironmentVariablesEl.is('allow-empty') + .is('regex', /^$|^([A-Za-z_][A-Za-z0-9_]*=[^,]+,?\s*)+$/) // MY_ENV_VAR=foo, ANOTHER_ENV_VAR=bar + .failure('Environment Variables must be in the form of KEY=VALUE or be empty'); + + let validateMountsEl = XNAT.validate(mountsEl).reset().chain(); + validateMountsEl.is('allow-empty') + .is('regex', /^$|^\/([A-Za-z0-9_\/-]+:\/[A-Za-z0-9_\/-]+:(ro|rw),?\s*)+$/) // /tools:/tools:ro, /home:/home:rw + .failure('Mounts must be in the form of /source:/target:(ro|rw) or be empty'); + + // validate fields + let errorMessages = []; + let validators = [validateNameEl, validateImageEl, validateDescriptionEl, validateCpuLimitEl, + validateCpuReservationEl, validateMemoryLimitEl, validateMemoryReservationEl, + validatePlacementConstraintsEl, validateGenericResourcesEl, + validateEnvironmentVariablesEl, validateMountsEl]; + + validators.forEach(validator => { + if (!validator.check()) { + validator.messages.forEach(message => errorMessages.push(message)); + } + }) + + if (errorMessages.length) { + // errors? + XNAT.dialog.open({ + title: 'Validation Error', + width: 500, + content: '
    • ' + errorMessages.join('
    • ') + '
    ', + }) + return; + } + + profile['name'] = nameEl.value; + profile['description'] = descriptionEl.value; + profile['task_template']['container_spec']['image'] = imageEl.value; + + profile['task_template']['resources']['cpu_limit'] = cpuLimitEl.value; + profile['task_template']['resources']['cpu_reservation'] = cpuReservationEl.value; + profile['task_template']['resources']['mem_limit'] = memoryLimitEl.value; + profile['task_template']['resources']['mem_reservation'] = memoryReservationEl.value; + profile['task_template']['placement']['constraints'] = placementConstraintsEl.value ? placementConstraintsEl.value.split(',').map(s => s.trim()).filter(s => s) : []; + profile['task_template']['resources']['generic_resources'] = genericResourcesEl.value ? Object.fromEntries(new Map(genericResourcesEl.value.split(',').map(s => s.trim().split('=')))) : {}; + profile['task_template']['container_spec']['env'] = environmentVariablesEl.value ? Object.fromEntries(new Map(environmentVariablesEl.value.split(',').map(s => s.trim().split('=')))) : {}; + + if (mountsEl.value) { + profile['task_template']['container_spec']['mounts'] = mountsEl.value.split(',').map(s => { + const [source, target, readonly] = s.trim().split(':'); + return { + 'source': source, + 'target': target, + 'read_only': readonly === 'ro', + 'type': 'bind' + } + }); + } else { + profile['task_template']['container_spec']['mounts'] = []; + } + + if (isNew) { + XNAT.plugin.jupyterhub.profiles.create(profile) + .then(() => { + refreshTable(); + XNAT.dialog.closeAll(); + XNAT.ui.banner.top(2000, 'Jupyter profile created', 'success'); + }) + .catch((err) => { + console.error(err); + XNAT.ui.banner.top(2000, 'Error creating profile', 'error'); + }); + } else { + XNAT.plugin.jupyterhub.profiles.update(profile) + .then(() => { + refreshTable(); + XNAT.dialog.closeAll(); + XNAT.ui.banner.top(2000, 'Jupyter profile updated', 'success'); + }) + .catch((err) => { + console.error(err); + XNAT.ui.banner.top(2000, 'Error updating profile', 'error'); + }); + } + } + }, + { + label: 'Cancel', + close: true + } + ] + }); + } + + const newProfileButton = spawn('button.new-profile.btn.btn-sm.submit', { + html: 'New Profile', + onclick: () => { + editor(); + } + }) + + footerEl.append(spawn('div.pull-right', [newProfileButton])); + footerEl.append(spawn('div.clear.clearfix')); + + return refreshTable().then(() => { + return { + refreshTable: refreshTable + } + }); + } + +})); \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js index a377ab3..0d27fef 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js @@ -28,14 +28,14 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s let newServerUrl = XNAT.plugin.jupyterhub.servers.newServerUrl = function(username, servername, xsiType, itemId, itemLabel, projectId, eventTrackingId, - dockerImage) { + profileId) { let url = `/xapi/jupyterhub/users/${username}/server`; // if (servername !== '') { // url = `${url}/${servername}`; // } - url = `${url}?xsiType=${xsiType}&itemId=${itemId}&itemLabel=${itemLabel}&eventTrackingId=${eventTrackingId}&dockerImage=${dockerImage}`; + url = `${url}?xsiType=${xsiType}&itemId=${itemId}&itemLabel=${itemLabel}&eventTrackingId=${eventTrackingId}&profileId=${profileId}`; if (projectId) url = `${url}&projectId=${projectId}`; return restUrl(url); @@ -181,66 +181,130 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s console.debug(`jupyterhub-servers.js: XNAT.plugin.jupyterhub.servers.startServer`); console.debug(`Launching jupyter server. User: ${username}, Server Name: ${servername}, XSI Type: ${xsiType}, ID: ${itemId}, Label: ${itemLabel}, Project ID: ${projectId}, eventTrackingId: ${eventTrackingId}`); - XNAT.plugin.jupyterhub.dockerImages.getImages().then(images => { + XNAT.plugin.jupyterhub.profiles.getAll().then(profiles => { + // filter out disabled configurations + profiles = profiles.filter(c => c['enabled'] === true); + + const cancelButton = { + label: 'Cancel', + isDefault: false, + close: true, + } + + const startButton = { + label: 'Start Jupyter', + isDefault: true, + close: true, + action: function(obj) { + const profile = obj.$modal.find('#profile').val(); + const profileId = profiles.filter(c => c['name'] === profile)[0]['id']; + + XNAT.xhr.ajax({ + url: newServerUrl(username, servername, xsiType, itemId, itemLabel, + projectId, eventTrackingId, profileId), + method: 'POST', + contentType: 'application/json', + beforeSend: function () { + XNAT.app.activityTab.start('Start Jupyter Notebook Server', eventTrackingId, + 'XNAT.plugin.jupyterhub.servers.activityTabCallback', 1000); + }, + fail: function (error) { + console.error(`Failed to send Jupyter server request: ${error}`) + } + }); + } + } + + const buttons = (profiles.length === 0) ? [cancelButton] : [cancelButton, startButton]; + XNAT.dialog.open({ - title: 'Jupyter Configuration', + title: 'Select Jupyter Profile', content: spawn('form'), maxBtn: true, - width: 600, + width: 750, beforeShow: function(obj) { - let imageOptions = images.filter(i => i['enabled'] ) - .map(i => i['image']) - .sort() - .map(i => [{value: i}]) - .flat(); - + if (profiles.length === 0) { + obj.$modal.find('.xnat-dialog-content').html('
    No Jupyter profiles are configured or enabled. Please contact your XNAT administrator.
    '); + return; + } + + let configOptions = profiles.map(c => c['name']) + .map(c => [{value: c}]) + .flat(); + // spawn new image form const formContainer$ = obj.$modal.find('.xnat-dialog-content'); formContainer$.addClass('panel'); + + let initialProfile = { + name: profiles[0]['name'], + description: profiles[0]['description'], + image: profiles[0]['task_template']['container_spec']['image'], + cpuLimit: profiles[0]['task_template']['resources']['cpu_limit'] ? profiles[0]['task_template']['resources']['cpu_limit'] : 'No Limit', + cpuReservation: profiles[0]['task_template']['resources']['cpu_reservation'] ? profiles[0]['task_template']['resources']['cpu_reservation'] : 'No Reservation', + get cpu() { + return `${this.cpuReservation} / ${this.cpuLimit}`; + }, + memoryLimit: profiles[0]['task_template']['resources']['mem_limit'] ? profiles[0]['task_template']['resources']['mem_limit'] : 'No Limit', + memoryReservation: profiles[0]['task_template']['resources']['mem_reservation'] ? profiles[0]['task_template']['resources']['mem_reservation'] : 'No Reservation', + get memory() { + return `${this.memoryReservation} / ${this.memoryLimit}`; + } + } + obj.$modal.find('form').append( spawn('!', [ XNAT.ui.panel.select.single({ - name: 'dockerImage', - id: 'dockerImage', - options: imageOptions, - label: 'Docker image', - validation: 'required not-empty', - description: 'Select a Docker image to use for your Jupyter notebook server.' - }).element + name: 'profile', + id: 'profile', + options: configOptions, + label: 'Profile', + required: true, + description: "Select the Jupyter profile to use. This will determine the Docker image, computing resources, and other configuration options used for your Jupyter notebook server." + }).element, + XNAT.ui.panel.element({ + label: 'Description', + html: `

    ${initialProfile.description}

    `, + }).element, + XNAT.ui.panel.element({ + label: 'Image', + html: `

    ${initialProfile.image}

    The Docker image that will be used to launch your Jupyter notebook server.
    `, + }).element, + XNAT.ui.panel.element({ + label: 'CPU Reservation / Limit', + html: `

    ${initialProfile.cpu}

    The reservation is the minimum amount of CPU resources that will be guaranteed to your server. The limit is the maximum amount of CPU resources that will be allocated to your server.
    `, + }).element, + XNAT.ui.panel.element({ + label: 'Memory Reservation / Limit', + html: `

    ${initialProfile.memory}

    The reservation is the minimum amount of memory resources that will be guaranteed to your server. The limit is the maximum amount of memory resources that will be allocated to your server.
    `, + }).element, ]) ); + + let profileSelector = document.getElementById('profile'); + profileSelector.addEventListener('change', () => { + let description = document.querySelector('.profile-description'); + let profile = profiles.filter(c => c['name'] === profileSelector.value)[0]; + description.innerHTML = profile['description']; + + let image = document.querySelector('.profile-image'); + image.innerHTML = profile['task_template']['container_spec']['image']; + + let cpu = document.querySelector('.profile-cpu'); + let cpuLimit = profile['task_template']['resources']['cpu_limit'] ? profile['task_template']['resources']['cpu_limit'] : 'No Limit'; + let cpuReservation = profile['task_template']['resources']['cpu_reservation'] ? profile['task_template']['resources']['cpu_reservation'] : 'No Reservation'; + cpu.innerHTML = `${cpuReservation} / ${cpuLimit}`; + + let memory = document.querySelector('.profile-memory'); + let memoryLimit = profile['task_template']['resources']['mem_limit'] ? profile['task_template']['resources']['mem_limit'] : 'No Limit'; + let memoryReservation = profile['task_template']['resources']['mem_reservation'] ? profile['task_template']['resources']['mem_reservation'] : 'No Reservation'; + memory.innerHTML = `${memoryReservation} / ${memoryLimit}`; + }); + }, - buttons: [ - { - label: 'Start Jupyter', - isDefault: true, - close: true, - action: function() { - const dockerImageEl = document.getElementById("dockerImage"); - - XNAT.xhr.ajax({ - url: newServerUrl(username, servername, xsiType, itemId, itemLabel, - projectId, eventTrackingId, dockerImageEl.value), - method: 'POST', - contentType: 'application/json', - beforeSend: function () { - XNAT.app.activityTab.start('Start Jupyter Notebook Server', eventTrackingId, - 'XNAT.plugin.jupyterhub.servers.activityTabCallback', 1000); - }, - fail: function (error) { - console.error(`Failed to send Jupyter server request: ${error}`) - } - }); - - } - }, - { - label: 'Cancel', - close: true - } - ] + buttons: buttons }); - }) + }); } let activityTabCallback = XNAT.plugin.jupyterhub.servers.activityTabCallback = function(itemDivId, detailsTag, jsonobj, lastProgressIdx) { diff --git a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm index 7bf1bb7..8e96e2e 100644 --- a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm +++ b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm @@ -8,7 +8,7 @@ - + diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index 732b4aa..c36caac 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -84,60 +84,6 @@ inactivityTimeout: description: > Automatically shut down idle Jupyter notebook servers if they've been inactive for some time. Set the timeout to 0 to not cull any inactive servers. -resourceSpecCpuLimit: - kind: panel.input.number - id: resourceSpecCpuLimit - name: resourceSpecCpuLimit - label: CPU Limit - validation: decimal gte:0.0 onblur - element: - step: 0.1 - description: > - (Optional) Maximum number of cpu-cores a single-user notebook server is allowed to use. If this value is set to 0.5, - allows use of 50% of one CPU. If this value is set to 2, allows use of up to 2 CPUs. Set to 0 for this preference - to be ignored. - -resourceSpecCpuReservation: - kind: panel.input.number - id: resourceSpecCpuReservation - name: resourceSpecCpuReservation - label: CPU Reservation - validation: decimal gte:0.0 onblur - element: - step: 0.1 - description: > - (Optional) Minimum number of cpu-cores a single-user notebook server is guaranteed to have available. If this value - is set to 0.5, allows use of 50% of one CPU. If this value is set to 2, allows use of up to 2 CPUs. Set to 0 for - this preference to be ignored. - -resourceSpecMemLimit: - kind: panel.input.text - id: resourceSpecMemLimit - name: resourceSpecMemLimit - label: Memory Limit - description: > - (Optional) Maximum number of bytes a single-user notebook server is allowed to use. Leave this input empty for this - preference to be ignored. - Allows the following suffixes: - K -> Kilobytes - M -> Megabytes - G -> Gigabytes - T -> Terabytes - -resourceSpecMemReservation: - kind: panel.input.text - id: resourceSpecMemReservation - name: resourceSpecMemReservation - label: Memory Reservation - description: > - (Optional) Minimum number of bytes a single-user notebook server is guaranteed to have available. Leave this input - empty for this preference to be ignored. - Allows the following suffixes: - K -> Kilobytes - M -> Megabytes - G -> Gigabytes - T -> Terabytes - jupyterHubSetup: kind: panel name: jupyterHubSetup @@ -163,8 +109,8 @@ userAuthorization: style: marginBottom: 24px html: - Users with access to Jupyter will be able to run arbitrary code from within their Jupyter notebook container. - Individual users may be authorized to start Jupyter containers or all users (excluding guest users) can be + Users with access to Jupyter will be able to run arbitrary code from within their Jupyter notebook container. + Individual users may be authorized to start Jupyter containers or all users (excluding guest users) can be authorized to start Jupyter containers. allowAllToggle: kind: panel.input.switchbox @@ -187,23 +133,26 @@ userAuthorization: content: > XNAT.plugin.jupyterhub.users.authorization.initTable('jupyterhub-user-auth-container') -imagePreferences: +jupyterHubProfiles: kind: panel - name: imagePreferences - label: Images + name: jupyterHubProfiles + label: JupyterHub Profiles contents: - imageDescription: + description: tag: div.message contents: - "This panel lists the containers which JupyterHub can use to launch single-user Jupyter notebook servers. JupyterHub supports any of the existing Jupyter docker stacks provided the version of JupyterHub in the image matches. Please see the documentation for building your own images. Pull each image on every swarm node." - imagesTable: - tag: "div#jupyterhub-image-preferences" - imagePreferencesScript: - tag: script|src="~/scripts/xnat/plugin/jupyterhub/jupyterhub-docker-images.js" - renderImagePreferencesTable: + "You can create configurations for multiple user environments and let users select from them when they launch a + Jupyter notebook server. This is done by creating multiple profiles, each of which is attached to a set of + configuration options that will be applied to the single-user server when it is launched. This can be used to + let users choose among Docker images or to select the hardware resources they want to use." + profilesTable: + tag: "div#jupyterhub-profiles-table" + profilesScript: + tag: script|src="~/scripts/xnat/plugin/jupyterhub/jupyterhub-profiles.js" + renderProfilesTable: tag: script content: > - XNAT.plugin.jupyterhub.dockerImages.init('#jupyterhub-image-preferences'); + XNAT.plugin.jupyterhub.profiles.manager('jupyterhub-profiles-table'); jupyterhubPreferences: kind: panel.form @@ -223,10 +172,6 @@ jupyterhubPreferences: ${pathTranslationXnatPrefix} ${pathTranslationDockerPrefix} ${workspacePath} - ${resourceSpecCpuLimit} - ${resourceSpecCpuReservation} - ${resourceSpecMemLimit} - ${resourceSpecMemReservation} jupyterHubUserActivity: kind: panel @@ -268,7 +213,7 @@ siteSettings: contents: ${jupyterHubSetup} ${userAuthorization} - ${imagePreferences} + ${jupyterHubProfiles} jupyterhubUserActivityTab: kind: tab name: jupyterhubUserActivityTab diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java index 2e4366d..5e90d28 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java @@ -4,6 +4,7 @@ import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xnatx.plugins.jupyterhub.client.JupyterHubClient; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultJupyterHubService; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; @@ -21,13 +22,15 @@ public DefaultJupyterHubService defaultJupyterHubService(final JupyterHubClient final PermissionsHelper mockPermissionsHelper, final UserOptionsService mockUserOptionsService, final JupyterHubPreferences mockJupyterHubPreferences, - final UserManagementServiceI mockUserManagementService) { + final UserManagementServiceI mockUserManagementService, + final ProfileService mockProfileService) { return new DefaultJupyterHubService(mockJupyterHubClient, mockNrgEventService, mockPermissionsHelper, mockUserOptionsService, mockJupyterHubPreferences, - mockUserManagementService); + mockUserManagementService, + mockProfileService); } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultProfileServiceConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultProfileServiceConfig.java new file mode 100644 index 0000000..8cd2ebd --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultProfileServiceConfig.java @@ -0,0 +1,18 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultProfileService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({MockConfig.class}) +public class DefaultProfileServiceConfig { + + @Bean + public DefaultProfileService defaultProfileService(final ProfileEntityService mockProfileEntityService) { + return new DefaultProfileService(mockProfileEntityService); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java index 2a61325..82c93d7 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java @@ -4,6 +4,7 @@ import org.nrg.xdat.security.services.SearchHelperServiceI; import org.nrg.xdat.services.AliasTokenService; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsEntityService; import org.nrg.xnatx.plugins.jupyterhub.services.UserWorkspaceService; import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultUserOptionsService; @@ -23,14 +24,16 @@ public DefaultUserOptionsService defaultUserOptionsService(final JupyterHubPrefe final AliasTokenService mockAliasTokenService, final SiteConfigPreferences mockSiteConfigPreferences, final UserOptionsEntityService mockUserOptionsEntityService, - final PermissionsHelper mockPermissionsHelper) { + final PermissionsHelper mockPermissionsHelper, + final ProfileService profileService) { return new DefaultUserOptionsService(mockJupyterHubPreferences, mockUserWorkspaceService, mockSearchHelperService, mockAliasTokenService, mockSiteConfigPreferences, mockUserOptionsEntityService, - mockPermissionsHelper); + mockPermissionsHelper, + profileService); } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfileInitializerConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfileInitializerConfig.java new file mode 100644 index 0000000..e7182c6 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfileInitializerConfig.java @@ -0,0 +1,20 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.xnatx.plugins.jupyterhub.initialize.JupyterHubProfileInitializer; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({MockConfig.class}) +public class JupyterHubProfileInitializerConfig { + + @Bean + public JupyterHubProfileInitializer defaultJupyterHubProfileInitializer(final ProfileService mockProfileService, + final XFTManagerHelper mockXFTManagerHelper) { + return new JupyterHubProfileInitializer(mockProfileService, mockXFTManagerHelper); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfilesApiConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfilesApiConfig.java new file mode 100644 index 0000000..ad7edf3 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfilesApiConfig.java @@ -0,0 +1,44 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.framework.services.ContextService; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnatx.plugins.jupyterhub.rest.JupyterHubProfilesApi; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.TestingAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration +@EnableWebMvc +@EnableWebSecurity +@Import({MockConfig.class, RestApiTestConfig.class}) +public class JupyterHubProfilesApiConfig extends WebSecurityConfigurerAdapter { + + @Bean + public JupyterHubProfilesApi jupyterHubProfilesApi(final UserManagementServiceI mockUserManagementService, + final RoleHolder mockRoleHolder, + final ProfileService mockProfileService) { + return new JupyterHubProfilesApi(mockUserManagementService, + mockRoleHolder, + mockProfileService); + } + + @Bean + public ContextService contextService(final ApplicationContext applicationContext) { + final ContextService contextService = new ContextService(); + contextService.setApplicationContext(applicationContext); + return contextService; + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) { + auth.authenticationProvider(new TestingAuthenticationProvider()); + } +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java index 0eec288..bcafcae 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java @@ -12,10 +12,7 @@ import org.nrg.xnat.tracking.services.EventTrackingDataHibernateService; import org.nrg.xnatx.plugins.jupyterhub.client.JupyterHubClient; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; -import org.nrg.xnatx.plugins.jupyterhub.services.JupyterHubService; -import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsEntityService; -import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; -import org.nrg.xnatx.plugins.jupyterhub.services.UserWorkspaceService; +import org.nrg.xnatx.plugins.jupyterhub.services.*; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; import org.springframework.beans.factory.annotation.Qualifier; @@ -71,6 +68,11 @@ public UserOptionsEntityService mockUserOptionsEntityService() { return Mockito.mock(UserOptionsEntityService.class); } + @Bean + public ProfileEntityService mockProfileEntityService() { + return Mockito.mock(ProfileEntityService.class); + } + @Bean public PermissionsServiceI mockPermissionsService() { return Mockito.mock(PermissionsServiceI.class); @@ -81,6 +83,11 @@ public UserOptionsService mockUserOptionsService() { return Mockito.mock(UserOptionsService.class); } + @Bean + public ProfileService mockProfileService() { + return Mockito.mock(ProfileService.class); + } + @Bean public JupyterHubClient mockJupyterHubClient() { return Mockito.mock(JupyterHubClient.class); @@ -134,7 +141,7 @@ public EventTrackingDataHibernateService mockEventTrackingDataHibernateService() } @Bean - public SerializerService serializerService() { + public SerializerService mockSerializerService() { return Mockito.mock(SerializerService.class); } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializerTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializerTest.java new file mode 100644 index 0000000..323a750 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializerTest.java @@ -0,0 +1,76 @@ +package org.nrg.xnatx.plugins.jupyterhub.initialize; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubProfileInitializerConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.Profile; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Collections; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = JupyterHubProfileInitializerConfig.class) +public class JupyterHubProfileInitializerTest { + + @Autowired private JupyterHubProfileInitializer jupyterHubProfileInitializer; + @Autowired private ProfileService mockProfileService; + @Autowired private XFTManagerHelper mockXFTManagerHelper; + + @After + public void after() { + Mockito.reset(mockProfileService); + Mockito.reset(mockXFTManagerHelper); + } + + @Test + public void getTaskName() { + assertNotNull(jupyterHubProfileInitializer.getTaskName()); + } + + @Test(expected = InitializingTaskException.class) + public void callImpl_xftManagerNotInitialized() throws Exception { + // XFT not initialized + when(mockXFTManagerHelper.isInitialized()).thenReturn(false); + + // Should throw InitializingTaskException + jupyterHubProfileInitializer.callImpl(); + } + + @Test + public void callImpl_profileServiceHasProfiles() throws Exception { + // Setup + // XFT initialized and profile service has profiles + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockProfileService.getAll()).thenReturn(Collections.singletonList(Profile.builder().build())); + + // Test + jupyterHubProfileInitializer.callImpl(); + + // Verify profile service not called + verify(mockProfileService, never()).create(any(Profile.class)); + } + + @Test + public void callImpl_initProfile() throws Exception { + // Setup + // XFT initialized and profile service has no profiles + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockProfileService.getAll()).thenReturn(Collections.emptyList()); + + // Test + jupyterHubProfileInitializer.callImpl(); + + // Verify profile service called + verify(mockProfileService, times(1)).create(any(Profile.class)); + } +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java index 309dae3..399f7ac 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java @@ -14,6 +14,7 @@ import org.nrg.xnatx.plugins.jupyterhub.client.models.*; import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubApiConfig; import org.nrg.xnatx.plugins.jupyterhub.models.*; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.*; import org.nrg.xnatx.plugins.jupyterhub.services.JupyterHubService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.springframework.beans.factory.annotation.Autowired; @@ -62,9 +63,7 @@ public class JupyterHubApiTest { private User nonAdmin_jh; private XnatUserOptions userOptions; - private ContainerSpec containerSpec; - private PlacementSpec placementSpec; - private ResourceSpec resourceSpec; + private Long profileId; private Server dummyServer; private String servername; private Server dummyNamedServer; @@ -108,35 +107,39 @@ public void before() throws Exception { Map environmentalVariables = new HashMap<>(); environmentalVariables.put("XNAT_HOST", "fake://localhost"); - DockerImage image = DockerImage.builder() - .image("xnat/jupyterhub-single-user:latest") - .enabled(true) - .build(); - - BindMount bindMount = BindMount.builder() - .name("TestProject") - .writable(false) - .containerHostPath("/home/someone/xnat/data/archive/TestProject") - .xnatHostPath("/data/xnat/archive/TestProject") - .jupyterHostPath("/data/TestProject") + Mount mount = Mount.builder() + .source("/home/someone/xnat/data/archive/TestProject") + .target("/data/TestProject") + .type("bind") + .readOnly(true) .build(); - containerSpec = ContainerSpec.builder() - .image(image.getImage()) + ContainerSpec containerSpec = ContainerSpec.builder() + .image("xnat/jupyterhub-single-user:latest") .labels(Collections.singletonMap("label", "label")) + .env(environmentalVariables) + .mounts(Collections.singletonList(mount)) .build(); - placementSpec = PlacementSpec.builder() + Placement placement = Placement.builder() .constraints(Collections.singletonList("engine.labels.instance.type==jupyter")) .build(); - resourceSpec = ResourceSpec.builder() + Resources resources = Resources.builder() .cpuLimit(4.0) .cpuReservation(4.0) .memLimit("16G") .memReservation("16G") .build(); + TaskTemplate taskTemplate = TaskTemplate.builder() + .containerSpec(containerSpec) + .placement(placement) + .resources(resources) + .build(); + + profileId = 2L; + userOptions = XnatUserOptions.builder() .userId(nonAdminId) .servername("") @@ -145,11 +148,7 @@ public void before() throws Exception { .itemLabel("TestProject") .projectId("TestProject") .eventTrackingId("20220822T201541799Z") - .containerSpec(containerSpec) - .placementSpec(placementSpec) - .resourceSpec(resourceSpec) - .environmentVariables(environmentalVariables) - .bindMounts(Collections.singletonList(bindMount)) + .taskTemplate(taskTemplate) .build(); dummyServer = Server.builder() @@ -426,7 +425,7 @@ public void testStartServer() throws Exception { .param("itemLabel", userOptions.getItemLabel()) .param("projectId", userOptions.getProjectId()) .param("eventTrackingId", userOptions.getEventTrackingId()) - .param("dockerImage", containerSpec.getImage()) + .param("profileId", profileId.toString()) .with(authentication(NONADMIN_AUTH)) .with(csrf()) .with(testSecurityContext()); @@ -440,7 +439,7 @@ public void testStartServer() throws Exception { eq(userOptions.getItemLabel()), eq(userOptions.getProjectId()), eq(userOptions.getEventTrackingId()), - eq(containerSpec.getImage())); + eq(profileId)); // Named Server verify(mockJupyterHubService, never()).startServer(any(UserI.class), anyString(), @@ -449,7 +448,7 @@ public void testStartServer() throws Exception { anyString(), anyString(), anyString(), - anyString()); + anyLong()); } @Test @@ -462,7 +461,7 @@ public void testStartNamedServer() throws Exception { .param("itemLabel", userOptions.getItemLabel()) .param("projectId", userOptions.getProjectId()) .param("eventTrackingId", userOptions.getEventTrackingId()) - .param("dockerImage", containerSpec.getImage()) + .param("profileId", profileId.toString()) .with(authentication(NONADMIN_AUTH)) .with(csrf()) .with(testSecurityContext()); @@ -476,7 +475,7 @@ public void testStartNamedServer() throws Exception { anyString(), anyString(), anyString(), - anyString()); + anyLong()); // Named Server verify(mockJupyterHubService, times(1)).startServer(eq(nonAdmin), @@ -486,7 +485,7 @@ public void testStartNamedServer() throws Exception { eq(userOptions.getItemLabel()), eq(userOptions.getProjectId()), eq(userOptions.getEventTrackingId()), - eq(containerSpec.getImage())); + eq(profileId)); } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApiTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApiTest.java new file mode 100644 index 0000000..8714f1f --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApiTest.java @@ -0,0 +1,318 @@ +package org.nrg.xnatx.plugins.jupyterhub.rest; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xft.security.UserI; +import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubProfilesApiConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.Profile; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.ContainerSpec; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebAppConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = JupyterHubProfilesApiConfig.class) +public class JupyterHubProfilesApiTest { + + @Autowired private UserManagementServiceI mockUserManagementService; + @Autowired private RoleServiceI mockRoleService; + @Autowired private WebApplicationContext wac; + @Autowired private ObjectMapper mapper; + @Autowired private ProfileService mockProfileService; + + private MockMvc mockMvc; + + private final MediaType JSON = MediaType.APPLICATION_JSON_UTF8; + + private Authentication mockAuthentication; + + private Profile profile; + + @Before + public void setUp() throws Exception { + // Setup mock web context + mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); + + // Setup user + String mockUsername = "mockUser"; + String mockPassword = "mockPassword"; + Integer mockUserId = 1; + UserI mockUser = Mockito.mock(UserI.class); + Mockito.when(mockUser.getID()).thenReturn(mockUserId); + Mockito.when(mockUser.getLogin()).thenReturn(mockUsername); + Mockito.when(mockUser.getUsername()).thenReturn(mockUsername); + Mockito.when(mockUser.getPassword()).thenReturn(mockPassword); + + when(mockRoleService.isSiteAdmin(mockUser)).thenReturn(false); + when(mockUserManagementService.getUser(mockUsername)).thenReturn(mockUser); + + mockAuthentication = new TestingAuthenticationToken(mockUser, mockPassword); + + // Setup profile + ContainerSpec containerSpec = ContainerSpec.builder() + .image("image") + .build(); + + TaskTemplate taskTemplate = TaskTemplate.builder() + .containerSpec(containerSpec) + .build(); + + profile = Profile.builder() + .id(1L) + .name("name") + .description("description") + .taskTemplate(taskTemplate) + .build(); + } + + @After + public void tearDown() { + Mockito.reset(mockUserManagementService); + Mockito.reset(mockRoleService); + Mockito.reset(mockProfileService); + Mockito.reset(mockRoleService); + Mockito.reset(mockProfileService); + } + + @Test + public void getProfile() throws Exception { + // Setup mock + when(mockProfileService.get(profile.getId())).thenReturn(Optional.of(profile)); + + // Test + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/jupyterhub/profiles/" + profile.getId()) + .accept(JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + final String response = mockMvc + .perform(request) + .andExpect(status().isOk()) + .andExpect(content().contentType(JSON)) + .andReturn() + .getResponse() + .getContentAsString(); + + Profile responseProfile = mapper.readValue(response, Profile.class); + + assertEquals(profile, responseProfile); + } + + @Test + public void getProfile_doesNotExist() throws Exception { + // Setup mock + when(mockProfileService.get(profile.getId())).thenReturn(Optional.empty()); + + // Test + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/jupyterhub/profiles/" + profile.getId()) + .accept(JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request) + .andExpect(status().isNotFound()) + .andReturn() + .getResponse() + .getContentAsString(); + } + + @Test + public void getAllProfiles() throws Exception { + // Setup mock + when(mockProfileService.getAll()).thenReturn(Collections.singletonList(profile)); + + // Test + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/jupyterhub/profiles") + .accept(JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + final String response = mockMvc + .perform(request) + .andExpect(status().isOk()) + .andExpect(content().contentType(JSON)) + .andReturn() + .getResponse() + .getContentAsString(); + + List responseProfiles = mapper.readValue(response, new TypeReference>() {}); + + assertEquals(Collections.singletonList(profile), responseProfiles); + } + + @Test + public void createProfile() throws Exception { + // Setup mock + when(mockProfileService.create(profile)).thenReturn(profile.getId()); + + // Test + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .post("/jupyterhub/profiles") + .accept(JSON) + .contentType(JSON) + .content(mapper.writeValueAsString(profile)) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + final String response = mockMvc + .perform(request) + .andExpect(status().isCreated()) + .andExpect(content().contentType(JSON)) + .andReturn() + .getResponse() + .getContentAsString(); + + Long responseProfileId = mapper.readValue(response, Long.class); + + assertEquals(profile.getId(), responseProfileId); + } + + @Test + public void updateProfile() throws Exception { + // Setup mock + when(mockProfileService.exists(profile.getId())).thenReturn(true); + + // Test + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .put("/jupyterhub/profiles/" + profile.getId()) + .accept(JSON) + .contentType(JSON) + .content(mapper.writeValueAsString(profile)) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify mock is called + verify(mockProfileService, times(1)).update(profile); + } + + @Test + public void updateProfile_doesNotExist() throws Exception { + // Setup mock + when(mockProfileService.exists(profile.getId())).thenReturn(false); + + // Test + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .put("/jupyterhub/profiles/" + profile.getId()) + .accept(JSON) + .contentType(JSON) + .content(mapper.writeValueAsString(profile)) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request) + .andExpect(status().isNotFound()) + .andReturn() + .getResponse() + .getContentAsString(); + } + + @Test + public void updateProfile_cantHaveDifferentId() throws Exception { + // Setup mock + when(mockProfileService.exists(profile.getId())).thenReturn(true); + + // Test + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .put("/jupyterhub/profiles/" + profile.getId()) + .accept(JSON) + .contentType(JSON) + .content(mapper.writeValueAsString(Profile.builder().id(2L).build())) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse() + .getContentAsString(); + } + + @Test + public void deleteProfile() throws Exception { + // Setup mock + when(mockProfileService.exists(profile.getId())).thenReturn(true); + + // Test + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .delete("/jupyterhub/profiles/" + profile.getId()) + .accept(JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify mock is called + verify(mockProfileService, times(1)).delete(profile.getId()); + } + + @Test + public void deleteProfile_doesNotExist() throws Exception { + // Setup mock + when(mockProfileService.exists(profile.getId())).thenReturn(false); + + // Test + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .delete("/jupyterhub/profiles/" + profile.getId()) + .accept(JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request) + .andExpect(status().isNotFound()) + .andReturn() + .getResponse() + .getContentAsString(); + } +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java index b7e6f0b..c997b21 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java @@ -25,6 +25,7 @@ import org.nrg.xnatx.plugins.jupyterhub.config.DefaultJupyterHubServiceConfig; import org.nrg.xnatx.plugins.jupyterhub.events.JupyterServerEventI; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsEntityService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; @@ -55,6 +56,7 @@ public class DefaultJupyterHubServiceTest { @Autowired private UserOptionsService mockUserOptionsService; @Autowired private JupyterHubPreferences mockJupyterHubPreferences; @Autowired private UserManagementServiceI mockUserManagementServiceI; + @Autowired private ProfileService mockProfileService; @Captor ArgumentCaptor jupyterServerEventCaptor; @Captor ArgumentCaptor tokenArgumentCaptor; @@ -64,10 +66,9 @@ public class DefaultJupyterHubServiceTest { private final String servername = ""; private final String eventTrackingId = "eventTrackingId"; private final String projectId = "TestProject"; - private final String dockerImage = "jupyter/scipy-notebook:hub-2.3.0"; + private final Long profileId = 2L; private User userWithServers; private User userNoServers; - private Server server; @Before public void before() throws org.nrg.xdat.security.user.exceptions.UserNotFoundException, UserInitException { @@ -77,7 +78,7 @@ public void before() throws org.nrg.xdat.security.user.exceptions.UserNotFoundEx when(user.getUsername()).thenReturn(username); when(mockUserManagementServiceI.getUser(eq(username))).thenReturn(user); - server = Server.builder() + Server server = Server.builder() .name(servername) .build(); @@ -180,7 +181,7 @@ public void testStartServer_cantReadProject() throws Exception { when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(false); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId , projectId, eventTrackingId, dockerImage); + jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId , projectId, eventTrackingId, profileId); // Verify failure to start event occurred verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); @@ -203,7 +204,7 @@ public void testStartServer_cantReadSubject() throws Exception { // Test String subjectId = "XNAT_S00001"; String subjectLabel = "Subject1"; - jupyterHubService.startServer(user, XnatSubjectdata.SCHEMA_ELEMENT_NAME, subjectId, subjectLabel, projectId, eventTrackingId, dockerImage); + jupyterHubService.startServer(user, XnatSubjectdata.SCHEMA_ELEMENT_NAME, subjectId, subjectLabel, projectId, eventTrackingId, profileId); //Verify failure to start event occurred verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); @@ -226,7 +227,7 @@ public void testStartServer_cantReadExperiment() throws Exception { // Test String experimentId = "XNAT_E00001"; String experimentLabel = "Experiment01"; - jupyterHubService.startServer(user, XnatExperimentdata.SCHEMA_ELEMENT_NAME, experimentId, experimentLabel, projectId, eventTrackingId, dockerImage); + jupyterHubService.startServer(user, XnatExperimentdata.SCHEMA_ELEMENT_NAME, experimentId, experimentLabel, projectId, eventTrackingId, profileId); //Verify failure to start event occurred verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); @@ -247,7 +248,7 @@ public void testStartServer_cantReadScan() throws Exception { when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(false); // Test - jupyterHubService.startServer(user, XnatImagescandata.SCHEMA_ELEMENT_NAME, "456", "Scan 456", projectId, eventTrackingId, dockerImage); + jupyterHubService.startServer(user, XnatImagescandata.SCHEMA_ELEMENT_NAME, "456", "Scan 456", projectId, eventTrackingId, profileId); //Verify failure to start event occurred verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); @@ -271,7 +272,7 @@ public void testStartServer_HubOffline() throws Exception { when(mockJupyterHubClient.getVersion()).thenThrow(RuntimeException.class); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, dockerImage); + jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); Thread.sleep(1000); // Async call, need to wait. Is there a better way to test this? // Verify failure to start event occurred @@ -296,7 +297,32 @@ public void testStartServer_serverAlreadyRunning() throws Exception { when(mockJupyterHubClient.getUser(anyString())).thenReturn(Optional.of(userWithServers)); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, dockerImage); + jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); + Thread.sleep(1000); // Async call, need to wait. Is there a better way to test this? + + // Verify failure to start event occurred + verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); + JupyterServerEventI capturedEvent = jupyterServerEventCaptor.getValue(); + assertEquals(JupyterServerEventI.Status.Failed, capturedEvent.getStatus()); + assertEquals(JupyterServerEventI.Operation.Start, capturedEvent.getOperation()); + + // Verify user options are not saved + verify(mockUserOptionsEntityService, never()).createOrUpdate(any()); + + // Verify no attempts to start a server + verify(mockJupyterHubClient, never()).startServer(any(), any(), any()); + } + + @Test(timeout = 2000) + public void testStartServer_profileIdDoesNotExist() throws Exception { + // Grant permissions + when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); + + // Profile ID does not exist + when(mockProfileService.exists(anyLong())).thenReturn(false); + + // Test + jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); Thread.sleep(1000); // Async call, need to wait. Is there a better way to test this? // Verify failure to start event occurred @@ -317,18 +343,21 @@ public void testStartServer_Timeout() throws UserNotFoundException, ResourceAlre // Grant permissions when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); + // Profile ID exists + when(mockProfileService.exists(anyLong())).thenReturn(true); + // To successfully start a server there should be no running servers at first. // A subsequent call should contain a server, but let's presume it is never ready when(mockJupyterHubClient.getUser(anyString())).thenReturn(Optional.of(userNoServers)); when(mockJupyterHubClient.getServer(anyString(), anyString())).thenReturn(Optional.empty()); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, dockerImage); + jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); Thread.sleep(3000); // Async call, need to wait. Is there a better way to test this? // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), - eq(projectId), eq(projectId), eq(dockerImage), eq(eventTrackingId)); + eq(projectId), eq(projectId), eq(profileId), eq(eventTrackingId)); // Verify JupyterHub start server request sent verify(mockJupyterHubClient, times(1)).startServer(eq(username), eq(""), any(UserOptions.class)); @@ -348,18 +377,21 @@ public void testStartServer_Success() throws Exception { // Grant permissions when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); + // Profile ID exists + when(mockProfileService.exists(anyLong())).thenReturn(true); + // To successfully start a server there should be no running servers at first. // A subsequent call should eventually return a server in the ready state when(mockJupyterHubClient.getUser(anyString())).thenReturn(Optional.of(userNoServers)); when(mockJupyterHubClient.getServer(anyString(), anyString())).thenReturn(Optional.of(Server.builder().ready(true).build())); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, dockerImage); + jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); Thread.sleep(2500); // Async call, need to wait. Is there a better way to test this? // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), - eq(projectId), eq(projectId), eq(dockerImage), eq(eventTrackingId)); + eq(projectId), eq(projectId), eq(profileId), eq(eventTrackingId)); // Verify JupyterHub start server request sent verify(mockJupyterHubClient, times(1)).startServer(eq(username), eq(""), any(UserOptions.class)); @@ -376,6 +408,9 @@ public void testStartServer_CreateUser_Success() throws Exception { // Grant permissions when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); + // Profile ID exists + when(mockProfileService.exists(anyLong())).thenReturn(true); + // To successfully start a server there should be no running servers at first. // A subsequent call should eventually return a server in the ready state // Start with a user who does not yet exist on JupyterHub @@ -384,7 +419,7 @@ public void testStartServer_CreateUser_Success() throws Exception { when(mockJupyterHubClient.getServer(anyString(), anyString())).thenReturn(Optional.of(Server.builder().ready(true).build())); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, dockerImage); + jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); Thread.sleep(2500); // Async call, need to wait. Is there a better way to test this? // Verify create user attempt @@ -392,7 +427,7 @@ public void testStartServer_CreateUser_Success() throws Exception { // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), - eq(projectId), eq(projectId), eq(dockerImage), eq(eventTrackingId)); + eq(projectId), eq(projectId), eq(profileId), eq(eventTrackingId)); // Verify JupyterHub start server request sent verify(mockJupyterHubClient, times(1)).startServer(eq(username), eq(""), any(UserOptions.class)); diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileServiceTest.java new file mode 100644 index 0000000..3b8db2e --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileServiceTest.java @@ -0,0 +1,285 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.xapi.exceptions.DataFormatException; +import org.nrg.xnatx.plugins.jupyterhub.config.DefaultProfileServiceConfig; +import org.nrg.xnatx.plugins.jupyterhub.entities.ProfileEntity; +import org.nrg.xnatx.plugins.jupyterhub.models.Profile; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.ContainerSpec; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.Placement; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.Resources; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; +import org.nrg.xnatx.plugins.jupyterhub.services.ProfileEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = DefaultProfileServiceConfig.class) +public class DefaultProfileServiceTest { + + @Autowired private DefaultProfileService defaultProfileService; + @Autowired private ProfileEntityService mockProfileEntityService; + + @After + public void tearDown() { + Mockito.reset(mockProfileEntityService); + } + + @Test + public void exists() { + // create a profileEntity with an id + final Long profileId = 1L; + final ProfileEntity profileEntity = new ProfileEntity(); + profileEntity.setId(profileId); + + // mock the profileEntityService.retrieve() method to return a profileEntity + Mockito.when(mockProfileEntityService.retrieve(profileId)).thenReturn(profileEntity); + + // call the method under test + final boolean profileExists = defaultProfileService.exists(profileId); + + // verify profileEntityService.retrieve() is called + Mockito.verify(mockProfileEntityService, Mockito.times(1)).retrieve(profileId); + + // verify the method under test returns true + assert(profileExists); + } + + @Test + public void doesNotExist() { + // set the profile id + final Long profileId = 1L; + + // mock the profileEntityService.retrieve() method to return null + Mockito.when(mockProfileEntityService.retrieve(anyLong())).thenReturn(null); + + // call the method under test + final boolean profileExists = defaultProfileService.exists(profileId); + + // verify profileEntityService.retrieve() is called + Mockito.verify(mockProfileEntityService, Mockito.times(1)).retrieve(profileId); + + // verify the method under test returns false + assert(!profileExists); + } + + @Test + public void create() throws Exception { + // Setup profile + final Profile profile = createProfile(null, "profile1", "description1", "image1"); + + final ProfileEntity profileEntity = new ProfileEntity(); + profileEntity.setId(1L); + profileEntity.setProfile(profile); + + // mock the profileEntityService.create() method to return a profileEntity + Mockito.when(mockProfileEntityService.create(any(ProfileEntity.class))).thenReturn(profileEntity); + + // call the method under test + final Long profileId = defaultProfileService.create(profile); + + // verify profileEntityService.create() is called + Mockito.verify(mockProfileEntityService, Mockito.times(1)).create(profileEntity); + + // verify the method under test returns the correct profile id + assert(profileId.equals(profileEntity.getId())); + + // verify the profile id is set + assert(profile.getId().equals(profileEntity.getId())); + } + + @Test(expected = DataFormatException.class) + public void create_invalid() throws Exception { + // Setup profile + final Profile profile = createProfile(null, null, null, null); + + // call the method under test + defaultProfileService.create(profile); + } + + @Test + public void get() { + // Setup profile + final Profile profile = createProfile(1L, "profile1", "description1", "image1"); + + final ProfileEntity profileEntity = new ProfileEntity(); + profileEntity.setId(profile.getId()); + profileEntity.setProfile(profile); + + // mock the profileEntityService.retrieve() method to return a profileEntity + Mockito.when(mockProfileEntityService.retrieve(profile.getId())).thenReturn(profileEntity); + + // call the method under test + final Optional profileOptional = defaultProfileService.get(profile.getId()); + + // verify profileEntityService.retrieve() is called + Mockito.verify(mockProfileEntityService, Mockito.times(1)).retrieve(profile.getId()); + + // verify the method under test returns the correct profile + assert(profileOptional.isPresent()); + assert(profileOptional.get().getId().equals(profile.getId())); + assert(profileOptional.get().getName().equals(profile.getName())); + } + + @Test + public void get_doesNotExist() { + // set the profile id + final Long profileId = 1L; + + // mock the profileEntityService.retrieve() method to return null + Mockito.when(mockProfileEntityService.retrieve(anyLong())).thenReturn(null); + + // call the method under test + final Optional profileOptional = defaultProfileService.get(profileId); + + // verify profileEntityService.retrieve() is called + Mockito.verify(mockProfileEntityService, Mockito.times(1)).retrieve(profileId); + + // verify the method under test returns an empty optional + assert(!profileOptional.isPresent()); + } + + @Test + public void getAll() { + // Setup profiles + final Profile profile1 = createProfile(1L, "profile1", "description1", "image1"); + final Profile profile2 = createProfile(2L, "profile2", "description2", "image2"); + + final ProfileEntity profileEntity1 = new ProfileEntity(); + profileEntity1.setId(profile1.getId()); + profileEntity1.setProfile(profile1); + + final ProfileEntity profileEntity2 = new ProfileEntity(); + profileEntity2.setId(profile2.getId()); + profileEntity2.setProfile(profile2); + + final List profileEntities = Arrays.asList(profileEntity1, profileEntity2); + + // mock the profileEntityService.getAll() method to return a list of profileEntities + Mockito.when(mockProfileEntityService.getAll()).thenReturn(profileEntities); + + // call the method under test + final List profiles = defaultProfileService.getAll(); + + // verify profileEntityService.getAll() is called + Mockito.verify(mockProfileEntityService, Mockito.times(1)).getAll(); + + // verify the method under test returns the correct list of profiles + assert(profiles.size() == 2); + assert(profiles.get(0).getId().equals(profile1.getId())); + assert(profiles.get(0).getName().equals(profile1.getName())); + assert(profiles.get(1).getId().equals(profile2.getId())); + assert(profiles.get(1).getName().equals(profile2.getName())); + } + + @Test + public void update() throws DataFormatException { + // Setup profile + final Long profileId = 1L; + + final Profile oldProfile = createProfile(profileId, "oldName", "oldDescription", "oldImage"); + final Profile updatedProfile = createProfile(profileId, "updatedName", "updatedDescription", "updatedImage"); + + // Setup profileEntity + final ProfileEntity profileEntity = new ProfileEntity(); + profileEntity.setId(profileId); + profileEntity.setProfile(oldProfile); + + // mock the profileEntityService.retrieve() method to return a profileEntity + Mockito.when(mockProfileEntityService.retrieve(profileId)).thenReturn(profileEntity); + + // call the method under test + defaultProfileService.update(updatedProfile); + + // verify profileEntityService.retrieve() is called + Mockito.verify(mockProfileEntityService, Mockito.times(1)).retrieve(profileId); + + // verify profileEntityService.update() is called + Mockito.verify(mockProfileEntityService, Mockito.times(1)).update(profileEntity); + + // verify the profileEntity has the updated profile + assert(profileEntity.getProfile().getId().equals(updatedProfile.getId())); + assert(profileEntity.getProfile().getName().equals(updatedProfile.getName())); + } + + @Test(expected = DataFormatException.class) + public void update_invalid() throws Exception { + // Setup profile + final Long profileId = 1L; + + final Profile updatedProfile = createProfile(profileId, null, null, null); + + // call the method under test + defaultProfileService.update(updatedProfile); + } + + @Test(expected = DataFormatException.class) + public void update_doesNotExist() throws Exception { + // Setup profile + final Long profileId = 1L; + + final Profile updatedProfile = createProfile(profileId, "updatedName", "updatedDescription", "updatedImage"); + + // call the method under test + defaultProfileService.update(updatedProfile); + } + + @Test + public void delete() { + // set profile id + final Long profileId = 1L; + + // call the method under test + defaultProfileService.delete(profileId); + + // verify profileEntityService.delete() is called + Mockito.verify(mockProfileEntityService, Mockito.times(1)).delete(profileId); + } + + private Profile createProfile(Long id, String name, String description, String image) { + ContainerSpec containerSpec = ContainerSpec.builder() + .image(image) + .mounts(Collections.emptyList()) + .env(Collections.emptyMap()) + .labels(Collections.emptyMap()) + .build(); + + Placement placement = Placement.builder() + .constraints(Collections.emptyList()) + .build(); + + Resources resources = Resources.builder() + .cpuLimit(null) + .cpuReservation(null) + .memLimit(null) + .memReservation(null) + .genericResources(Collections.emptyMap()) + .build(); + + TaskTemplate taskTemplate = TaskTemplate.builder() + .containerSpec(containerSpec) + .placement(placement) + .resources(resources) + .build(); + + return Profile.builder() + .id(id) + .name(name) + .description(description) + .taskTemplate(taskTemplate) + .build(); + } + +} \ No newline at end of file From 4d2df44a76bd24bd6df651242ce38c355137f85c Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 27 Feb 2023 12:49:20 -0600 Subject: [PATCH 05/49] JHP-20: Corrects default profile Docker image --- .../jupyterhub/initialize/JupyterHubProfileInitializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java index 853c95a..53346d2 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java @@ -61,7 +61,7 @@ protected void callImpl() throws InitializingTaskException { private Profile buildDefaultProfile() { ContainerSpec containerSpec = ContainerSpec.builder() - .image("jupyterhub/datascience-notebook:hub-3.0.0") + .image("jupyter/datascience-notebook:hub-3.0.0") .mounts(Collections.emptyList()) .env(Collections.emptyMap()) .labels(Collections.emptyMap()) From 439ab6af874d1e7cb1df6d720dcf904e65d1375c Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 27 Feb 2023 14:58:10 -0600 Subject: [PATCH 06/49] JHP-36: Adds background process link --- .../plugin/jupyterhub/jupyterhub-servers.js | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js index 0d27fef..c2893e1 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js @@ -205,7 +205,10 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s method: 'POST', contentType: 'application/json', beforeSend: function () { - XNAT.app.activityTab.start('Start Jupyter Notebook Server', eventTrackingId, + XNAT.app.activityTab.start( + 'Start Jupyter Notebook Server' + + `
    `, + eventTrackingId, 'XNAT.plugin.jupyterhub.servers.activityTabCallback', 1000); }, fail: function (error) { @@ -346,6 +349,25 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s if (messages) { $(detailsTag).append(messages); } + + if (succeeded) { + XNAT.plugin.jupyterhub.users.getUser(window.username).then(user => { + let servers = user['servers'] || {}; + let eventTrackingId = jsonobj['key']; + + Object.entries(servers).forEach(([severName, server]) => { + let user_options = server['user_options'] || {}; + let serverEventTrackingId = user_options['eventTrackingId'] || ""; + + if (serverEventTrackingId && serverEventTrackingId === eventTrackingId) { + let openNbLink = document.getElementById(`open-nb-${eventTrackingId}`); + openNbLink.style.display = 'inline'; + openNbLink.addEventListener('click', () => XNAT.plugin.jupyterhub.servers.goTo(server['url'])); + } + }); + }) + } + return {succeeded: succeeded, lastProgressIdx: lastProgressIdx}; } From dc139cadc5dae25bde7941f9b0292e9682399135 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Tue, 28 Feb 2023 14:23:31 -0600 Subject: [PATCH 07/49] JHP-27: Adds a maxServerLifetime preferences which will stop long-running servers --- .../plugins/jupyterhub/JupyterHubPlugin.java | 7 +- .../preferences/JupyterHubPreferences.java | 16 +++ .../rest/JupyterHubPreferencesApi.java | 4 + .../services/JupyterHubService.java | 1 + .../impl/DefaultJupyterHubService.java | 85 ++++++++++---- .../spawner/jupyterhub/site-settings.yaml | 16 ++- .../impl/DefaultJupyterHubServiceTest.java | 110 ++++++++++++++++-- 7 files changed, 206 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java index 78ee87c..3333bd9 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java @@ -58,7 +58,12 @@ public JupyterHubClient getJupyterHubClient() { @Bean public TriggerTask cullIdleServers(final JupyterHubService jupyterHubService) { - return new TriggerTask(jupyterHubService::cullInactiveServers, new PeriodicTrigger(15, TimeUnit.MINUTES)); + return new TriggerTask(jupyterHubService::cullInactiveServers, new PeriodicTrigger(5, TimeUnit.MINUTES)); + } + + @Bean + public TriggerTask cullLongRunningServers(final JupyterHubService jupyterHubService) { + return new TriggerTask(jupyterHubService::cullLongRunningServers, new PeriodicTrigger(5, TimeUnit.MINUTES)); } } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java index 636c684..b7e7c05 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java @@ -31,6 +31,7 @@ public class JupyterHubPreferences extends AbstractPreferenceBean { public static final String RESOURCE_SPEC_MEM_LIMIT_PREF_ID = "resourceSpecMemLimit"; public static final String RESOURCE_SPEC_MEM_RESERVATION_PREF_ID = "resourceSpecMemReservation"; public static final String INACTIVITY_TIMEOUT_PREF_ID = "inactivityTimeout"; + public static final String MAX_SERVER_LIFETIME_PREF_ID = "maxServerLifetime"; @Autowired protected JupyterHubPreferences(NrgPreferenceService preferenceService, ConfigPaths configFolderPaths, OrderedProperties initPrefs) { @@ -262,6 +263,7 @@ public void setResourceSpecMemReservation(final String resourceSpecMemReservatio } } + // Inactivity timeout in minutes @NrgPreference(defaultValue = "60") public long getInactivityTimeout() { return getLongValue(INACTIVITY_TIMEOUT_PREF_ID); @@ -275,6 +277,20 @@ public void setInactivityTimeout(final long inactivityTimeout) { } } + // Max server lifetime in hours + @NrgPreference(defaultValue = "48") + public long getMaxServerLifetime() { + return getLongValue(MAX_SERVER_LIFETIME_PREF_ID); + } + + public void setMaxServerLifetime(final long maxServerLifetime) { + try { + setLongValue(maxServerLifetime, MAX_SERVER_LIFETIME_PREF_ID); + } catch (InvalidPreferenceName e) { + log.error("Invalid preference name 'maxServerLifetime': something is very wrong here.", e); + } + } + @NrgPreference(defaultValue = "false") public boolean getAllUsersCanStartJupyter() { return getBooleanValue(ALL_USERS_JUPYTER); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java index 7fba89b..29cf1a0 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java @@ -115,6 +115,10 @@ public Map getSpecifiedPreference(@ApiParam(value = "The Jupyter value = jupyterHubPreferences.getInactivityTimeout(); break; } + case (JupyterHubPreferences.MAX_SERVER_LIFETIME_PREF_ID): { + value = jupyterHubPreferences.getMaxServerLifetime(); + break; + } case (JupyterHubPreferences.ALL_USERS_JUPYTER): { value = jupyterHubPreferences.getAllUsersCanStartJupyter(); break; diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java index 51e299c..bfe8870 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java @@ -24,5 +24,6 @@ public interface JupyterHubService { void stopServer(UserI user, String servername, String eventTrackingId); Token createToken(UserI user, String note, Integer expiresIn); void cullInactiveServers(); + void cullLongRunningServers(); } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java index aa44607..d287e8c 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java @@ -392,29 +392,72 @@ public Token createToken(UserI user, String note, Integer expiresIn) { */ @Override public void cullInactiveServers() { - if (jupyterHubPreferences.getInactivityTimeout() > 0) { - log.debug("Culling idle Jupyter notebook servers"); - - final List users = getUsers(); - final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")); - - users.forEach(user -> { - Map servers = user.getServers(); - servers.forEach((servername, server) -> { - final ZonedDateTime lastActivity = server.getLast_activity(); - long inactiveTime = ChronoUnit.MINUTES.between(lastActivity, now); - - if (inactiveTime > jupyterHubPreferences.getInactivityTimeout()) { - try { - UserI userI = userManagementService.getUser(user.getName()); - log.info("Removing Jupyter server {} for user {} due to inactivity.", servername, user.getName()); - stopServer(userI, servername, now + "_cullIdleServers"); - } catch (UserInitException | org.nrg.xdat.security.user.exceptions.UserNotFoundException e) { - log.error("Unable to delete long running Jupyter server for user " + user.getName(), e); + try { + if (jupyterHubPreferences.getInactivityTimeout() > 0) { + log.debug("Culling idle Jupyter notebook servers"); + + final List users = getUsers(); + final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")); + + users.forEach(user -> { + Map servers = user.getServers(); + servers.forEach((servername, server) -> { + final ZonedDateTime lastActivity = server.getLast_activity(); + long inactiveTime = ChronoUnit.MINUTES.between(lastActivity, now); + + if (inactiveTime > jupyterHubPreferences.getInactivityTimeout()) { + try { + UserI userI = userManagementService.getUser(user.getName()); + log.info("Removing Jupyter server {} for user {} due to inactivity.", servername, user.getName()); + stopServer(userI, servername, now + "_cullIdleServers"); + } catch (UserInitException | org.nrg.xdat.security.user.exceptions.UserNotFoundException e) { + log.error("Unable to delete long running Jupyter server for user " + user.getName(), e); + } } - } + }); }); - }); + } else { + log.debug("Not culling idle Jupyter notebook servers"); + } + } catch (Exception e) { + log.error("Failed to cull idle Jupyter notebook servers"); + } + } + + /** + * Stop servers that have been running for some period of time + */ + @Override + public void cullLongRunningServers() { + try { + if (jupyterHubPreferences.getMaxServerLifetime() > 0) { + log.debug("Culling long running Jupyter notebook servers"); + + final List users = getUsers(); + final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")); + + users.forEach(user -> { + Map servers = user.getServers(); + servers.forEach((servername, server) -> { + final ZonedDateTime started = server.getStarted(); + long runningTime = ChronoUnit.HOURS.between(started, now); + + if (runningTime >= jupyterHubPreferences.getMaxServerLifetime()) { + try { + UserI userI = userManagementService.getUser(user.getName()); + log.info("Removing Jupyter server {} for user {} due to long running time.", servername, user.getName()); + stopServer(userI, servername, now + "_cullLongRunningServers"); + } catch (UserInitException | org.nrg.xdat.security.user.exceptions.UserNotFoundException e) { + log.error("Unable to delete long running Jupyter server for user " + user.getName(), e); + } + } + }); + }); + } else { + log.debug("Not culling long running Jupyter notebook servers"); + } + } catch (Exception e) { + log.error("Failed to cull long running Jupyter notebook servers"); } } diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index c36caac..677be64 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -79,10 +79,21 @@ inactivityTimeout: kind: panel.input.number id: inactivityTimeout name: inactivityTimeout - label: Inactivity Timeout (minutes) + label: Inactivity Timeout + afterElement: minutes validation: integer gte:0 onblur description: > - Automatically shut down idle Jupyter notebook servers if they've been inactive for some time. Set the timeout to 0 to not cull any inactive servers. + Automatically shut down idle Jupyter notebook servers if they've been inactive for some time. Set to 0 to not shut down any inactive servers. + +maxServerLifetime: + kind: panel.input.number + id: maxServerLifetime + name: maxServerLifetime + label: Max Server Lifetime + afterElement: hours + validation: integer gte:0 onblur + description: > + Automatically shut down Jupyter notebook servers after a certain amount of time regardless of activity. Set to 0 to not shut down any long running servers. jupyterHubSetup: kind: panel @@ -169,6 +180,7 @@ jupyterhubPreferences: ${stopTimeout} ${stopPollingInterval} ${inactivityTimeout} + ${maxServerLifetime} ${pathTranslationXnatPrefix} ${pathTranslationDockerPrefix} ${workspacePath} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java index c997b21..a9ce402 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java @@ -539,24 +539,51 @@ public void testCullIdleServers_Cull() throws Exception { verify(mockJupyterHubClient, times(1)).stopServer(eq(username), eq(server_inactive.getName())); } - @Test + @Test(timeout = 3000) public void testCullIdleServers_NoCull() throws InterruptedException { // Setup -> timeout set to zero when(mockJupyterHubPreferences.getInactivityTimeout()).thenReturn(0L); + // Test + jupyterHubService.cullInactiveServers(); + Thread.sleep(2000); // Stop server is async call, need to wait. + + // Verify no servers stopped + verify(mockJupyterHubClient, never()).stopServer(any(), any()); + } + + @Test(timeout = 3000) + public void testCullIdleServers_Exception() throws InterruptedException { + // Setup + when(mockJupyterHubPreferences.getInactivityTimeout()).thenReturn(90L); + when(mockJupyterHubClient.getUsers()).thenThrow(new RuntimeException("Unable to connect to JupyterHub")); + + // Test + jupyterHubService.cullInactiveServers(); + Thread.sleep(2000); // Stop server is async call, need to wait. + + // Verify no servers stopped + verify(mockJupyterHubClient, never()).stopServer(any(), any()); + } + + @Test(timeout = 3000) + public void testCullLongRunningServers_Cull() throws InterruptedException { + // Setup + when(mockJupyterHubPreferences.getMaxServerLifetime()).thenReturn(48L); + Server server_active = Server.builder() .name("server_active") - .last_activity(ZonedDateTime.now(ZoneId.of("UTC"))) + .started(ZonedDateTime.now(ZoneId.of("UTC"))) .build(); - Server server_inactive = Server.builder() - .name("server_inactive") - .last_activity(ZonedDateTime.now(ZoneId.of("UTC")).minusHours(2L)) + Server server_long_running = Server.builder() + .name("server_long_running") + .started(ZonedDateTime.now(ZoneId.of("UTC")).minusHours(48L).minusMinutes(1L)) .build(); Map servers = new HashMap<>(); - servers.put("server_active", server_active); - servers.put("server_inactive", server_inactive); + servers.put(server_active.getName(), server_active); + servers.put(server_long_running.getName(), server_long_running); User user = User.builder() .name(username) @@ -566,14 +593,79 @@ public void testCullIdleServers_NoCull() throws InterruptedException { when(mockJupyterHubClient.getUsers()).thenReturn(Collections.singletonList(user)); // Test - jupyterHubService.cullInactiveServers(); + jupyterHubService.cullLongRunningServers(); + Thread.sleep(2000); // Stop server is async call, need to wait. + + // Verify active server not stopped + verify(mockJupyterHubClient, never()).stopServer(eq(username), eq(server_active.getName())); + + // Verify inactive server stopped + verify(mockJupyterHubClient, times(1)).stopServer(eq(username), eq(server_long_running.getName())); + } + + @Test(timeout = 3000) + public void testCullLongRunningServers_NoCull_Disabled() throws InterruptedException { + // Setup -> timeout set to zero + when(mockJupyterHubPreferences.getMaxServerLifetime()).thenReturn(0L); + + // Test + jupyterHubService.cullLongRunningServers(); + Thread.sleep(2000); // Stop server is async call, need to wait. + + // Verify servers not stopped + verify(mockJupyterHubClient, never()).stopServer(any(), any()); + } + + @Test(timeout = 3000) + public void testCullLongRunningServers_NoCull() throws InterruptedException { + // Setup + when(mockJupyterHubPreferences.getMaxServerLifetime()).thenReturn(48L); + + Server server_active = Server.builder() + .name("server_active") + .started(ZonedDateTime.now(ZoneId.of("UTC"))) + .build(); + + // Server started less than 48 hours ago + Server server_long_running = Server.builder() + .name("server_long_running") + .started(ZonedDateTime.now(ZoneId.of("UTC")).minusHours(47L).minusMinutes(59L)) + .build(); + + Map servers = new HashMap<>(); + servers.put(server_active.getName(), server_active); + servers.put(server_long_running.getName(), server_long_running); + + User user = User.builder() + .name(username) + .servers(servers) + .build(); + + when(mockJupyterHubClient.getUsers()).thenReturn(Collections.singletonList(user)); + + // Test + jupyterHubService.cullLongRunningServers(); Thread.sleep(2000); // Stop server is async call, need to wait. // Verify active server not stopped verify(mockJupyterHubClient, never()).stopServer(eq(username), eq(server_active.getName())); // Verify inactive server not stopped. Timeout is set to zero - verify(mockJupyterHubClient, never()).stopServer(eq(username), eq(server_inactive.getName())); + verify(mockJupyterHubClient, never()).stopServer(eq(username), eq(server_long_running.getName())); } + @Test(timeout = 3000) + public void testCullLongRunningServers_Exception() throws InterruptedException { + // Setup + when(mockJupyterHubPreferences.getMaxServerLifetime()).thenReturn(48L); + when(mockJupyterHubClient.getUsers()).thenThrow(new RuntimeException("Unable to connect to JupyterHub")); + + // Test + jupyterHubService.cullLongRunningServers(); + Thread.sleep(2000); // Stop server is async call, need to wait. + + // Verify no server stopped + verify(mockJupyterHubClient, never()).stopServer(any(), any()); + + } } \ No newline at end of file From 792e3e5b03007cf04890c5e4b1401bd21463b6f6 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Tue, 28 Feb 2023 17:31:19 -0600 Subject: [PATCH 08/49] JHP-38: Hide polling interval timeouts from the ui --- .../xnat/plugin/jupyterhub/jupyterhub-hub.js | 31 ++++++++++++++----- .../spawner/jupyterhub/site-settings.yaml | 9 +++--- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js index 54004ed..d5c89cb 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js @@ -66,7 +66,7 @@ XNAT.plugin.jupyterhub.hub = getObject(XNAT.plugin.jupyterhub.hub || {}); XNAT.plugin.jupyterhub.hub.renderSetupForm = function(container_id) { console.debug(`jupyterhub-hub.js: XNAT.plugin.jupyterhub.hub.setupForm`); - XNAT.spawner + return XNAT.spawner .resolve('jupyterhub:siteSettings/jupyterhubPreferences') .ok(function(){ this.render(`#${container_id}`) }); } @@ -95,22 +95,39 @@ XNAT.plugin.jupyterhub.hub = getObject(XNAT.plugin.jupyterhub.hub || {}); return spawn('button.btn.sm.edit', { onclick: function(e) { e.preventDefault(); - let dialog = XNAT.dialog.open({ - title: '', + XNAT.dialog.open({ + title: 'JupyterHub Setup', content: spawn('div#jupyterhub-setup-form'), maxBtn: true, footer: false, - width: 750, + width: 800, beforeShow: function(obj) { console.log(obj) let serverFormContainerEl = document.getElementById(`jupyterhub-setup-form`); serverFormContainerEl.innerHTML = ''; - XNAT.plugin.jupyterhub.hub.renderSetupForm(`jupyterhub-setup-form`); + XNAT.plugin.jupyterhub.hub.renderSetupForm(`jupyterhub-setup-form`) + .ok(() => { + const formContainerEl = document.getElementById('jupyterhub-setup-form'); + const form = formContainerEl.querySelector('form'); + const saveButton = formContainerEl.querySelector('button.save'); + const revertButton = formContainerEl.querySelector('button.revert'); + + saveButton.addEventListener("click", () => { + setTimeout(() => { + if (!form.classList.contains('error')) { + XNAT.plugin.jupyterhub.hub.refreshSetupTable(hubSetupContainerId) + XNAT.ui.dialog.closeAll(); + } + }, 500); + }); + + revertButton.addEventListener("click", () => { + XNAT.ui.dialog.closeAll(); + }); + }) }, buttons: [] }) - let closeButton = dialog.$modal[0].querySelector('b.close'); - closeButton.addEventListener("click", () => XNAT.plugin.jupyterhub.hub.refreshSetupTable(hubSetupContainerId)) } }, 'Edit'); } diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index 677be64..5453316 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -28,9 +28,10 @@ startTimeout: id: startTimeout name: startTimeout label: Start Timeout + afterElement: seconds validation: integer gte:1 onblur description: > - Timeout (in seconds) before giving up on starting of single-user server. XNAT will assume that startup has failed if it takes longer than this. + Amount of time (in seconds) before giving up on starting a single-user server. XNAT will assume that startup has failed if it takes longer than this. startPollingInterval: kind: panel.input.number @@ -46,9 +47,10 @@ stopTimeout: id: stopTimeout name: stopTimeout label: Stop Timeout + afterElement: seconds validation: integer gte:1 onblur description: > - Timeout (in seconds) before giving up on stopping a single-user server. XNAT will assume that stopping has failed if it takes longer than this. + Amount of time (in seconds) before giving up on stopping a single-user server. XNAT will assume that stopping has failed if it takes longer than this. stopPollingInterval: kind: panel.input.number @@ -168,7 +170,6 @@ jupyterHubProfiles: jupyterhubPreferences: kind: panel.form name: jupyterhubPreferences - label: JupyterHub Setup method: POST contentType: json url: /xapi/jupyterhub/preferences @@ -176,9 +177,7 @@ jupyterhubPreferences: ${jupyterhubApiUrl} ${jupyterHubToken} ${startTimeout} - ${startPollingInterval} ${stopTimeout} - ${stopPollingInterval} ${inactivityTimeout} ${maxServerLifetime} ${pathTranslationXnatPrefix} From de2dd8ab30dfcc6e1195d1b02ff7a9ab08a29723 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Thu, 2 Mar 2023 11:29:45 -0600 Subject: [PATCH 09/49] JHP-39: Adds Jupyter and JupyterHub role to the Edit User Info panel --- .../roles/ jupyter-role-definition.properties | 15 +++++++++++++++ .../roles/jupyterhub-role-definition.properties | 15 +++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/main/resources/config/roles/ jupyter-role-definition.properties create mode 100644 src/main/resources/config/roles/jupyterhub-role-definition.properties diff --git a/src/main/resources/config/roles/ jupyter-role-definition.properties b/src/main/resources/config/roles/ jupyter-role-definition.properties new file mode 100644 index 0000000..736fca4 --- /dev/null +++ b/src/main/resources/config/roles/ jupyter-role-definition.properties @@ -0,0 +1,15 @@ +# +# xnat-jupyterhub-plugin: jupyter-role-definition.properties +# XNAT http://www.xnat.org +# Copyright (c) 2005-2023, Washington University School of Medicine +# All Rights Reserved +# +# Released under the Simplified BSD. +# + +##DO NOT USE commas in this file +org.nrg.Role=Jupyter +org.nrg.Role.Jupyter.key=Jupyter +org.nrg.Role.Jupyter.name=Jupyter Access +org.nrg.Role.Jupyter.warning=Users with access to Jupyter will be able to run arbitrary code from within their Jupyter containers. +org.nrg.Role.Jupyter.description=This is a custom user role for users who should be able to start Jupyter from various XNAT data types. Users given this role will be able to launch Jupyter notebooks containers and have read access to the data types they are launched from. This role is not required if the 'All Users Can Start Jupyter' plugin setting is set to true. diff --git a/src/main/resources/config/roles/jupyterhub-role-definition.properties b/src/main/resources/config/roles/jupyterhub-role-definition.properties new file mode 100644 index 0000000..cfc83df --- /dev/null +++ b/src/main/resources/config/roles/jupyterhub-role-definition.properties @@ -0,0 +1,15 @@ +# +# xnat-jupyterhub-plugin: jupyterhub-role-definition.properties +# XNAT http://www.xnat.org +# Copyright (c) 2005-2023, Washington University School of Medicine +# All Rights Reserved +# +# Released under the Simplified BSD. +# + +##DO NOT USE commas in this file +org.nrg.Role=JupyterHub +org.nrg.Role.JupyterHub.key=JupyterHub +org.nrg.Role.JupyterHub.name=JupyterHub Service Account +org.nrg.Role.JupyterHub.warning=This is a service account role for JupyterHub. You do not need to assign this role to any users. +org.nrg.Role.JupyterHub.description=This is a service account role for JupyterHub. It is used by JupyterHub to retrieve the Jupyter container configuration from XNAT when a user launches a Jupyter notebook container. You do not need to assign this role to any users. From 4d05a46ee70f141b2e8c5ba84489a6621f2ead1e Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 6 Mar 2023 15:11:59 -0600 Subject: [PATCH 10/49] JHP-33: Adds XDG_CONFIG_HOME env variable for gitconfig file in the user's workspace directory Store file at /workspace/{username}/git/config --- .../services/impl/DefaultUserOptionsService.java | 1 + .../services/impl/DefaultUserWorkspaceService.java | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java index e0302bc..294430f 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java @@ -389,6 +389,7 @@ protected Map getDefaultEnvironmentVariables(UserI user, String defaultEnvironmentVariables.put("XNAT_XSI_TYPE", xsiType); defaultEnvironmentVariables.put("XNAT_ITEM_ID", id); defaultEnvironmentVariables.put("JUPYTERHUB_ROOT_DIR", Paths.get("/workspace", user.getUsername()).toString()); + defaultEnvironmentVariables.put("XDG_CONFIG_HOME", Paths.get("/workspace", user.getUsername()).toString()); return defaultEnvironmentVariables; } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserWorkspaceService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserWorkspaceService.java index 39fae60..8b7004c 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserWorkspaceService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserWorkspaceService.java @@ -42,6 +42,19 @@ public Path getUserWorkspace(final UserI user) { log.error("Unable to create Jupyter notebook workspace for user " + user.getUsername(), e); throw new RuntimeException(e); } + + try { + // write a gitconfig file to the workspace + final Path gitconfigPath = Paths.get(userWorkspacePath.toString(), "git", "config"); + Files.createDirectories(gitconfigPath.getParent()); + Files.createFile(gitconfigPath); + final String gitconfig = "[user]\n" + + " name = " + user.getUsername() + "\n" + + " email = " + user.getEmail(); + Files.write(gitconfigPath, gitconfig.getBytes()); + } catch (IOException e) { + log.error("Unable to create gitconfig file for user " + user.getUsername(), e); + } } return userWorkspacePath; From 37ce573894aa0064c1143214ceaff69a93243079 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 6 Mar 2023 16:19:56 -0600 Subject: [PATCH 11/49] JHP-40: Remove automatic Jupyter access for admins --- .../JupyterUserAuthorization.java | 4 +-- .../jupyterhub/rest/JupyterHubApi.java | 3 +- .../jupyterhub/jupyterhub-preferences.js | 10 +++--- .../plugin/jupyterhub/jupyterhub-user-auth.js | 31 +++++++++---------- .../JupyterUserAuthorizationTest.java | 7 ++--- 5 files changed, 24 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java index 55e28bc..af24bbb 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java @@ -38,9 +38,7 @@ protected boolean considerGuests() { } protected boolean checkJupyter(UserI user) { - return jupyterHubPreferences.getAllUsersCanStartJupyter() - || roleHolder.checkRole(user, JUPYTER_ROLE) - || roleHolder.isSiteAdmin(user); + return jupyterHubPreferences.getAllUsersCanStartJupyter() || roleHolder.checkRole(user, JUPYTER_ROLE); } } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java index 63bad23..ade3e72 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java @@ -245,7 +245,8 @@ public void stopServer(@ApiParam(value = "username", required = true) @PathVaria @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized."), @ApiResponse(code = 500, message = "Unexpected error")}) - @XapiRequestMapping(value = "/users/{username}/tokens", method = POST, restrictTo = AccessLevel.User) + @AuthDelegate(JupyterUserAuthorization.class) + @XapiRequestMapping(value = "/users/{username}/tokens", method = POST, restrictTo = Authorizer) public Token createToken(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, @ApiParam(value = "note", required = true) @RequestParam("note") final String note, @ApiParam(value = "expiresIn", required = true) @RequestParam(value = "expiresIn") final Integer expiresIn) throws UserNotFoundException, UserInitException { diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-preferences.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-preferences.js index d817bd1..7a5aaec 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-preferences.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-preferences.js @@ -23,14 +23,13 @@ XNAT.plugin.jupyterhub.preferences = getObject(XNAT.plugin.jupyterhub.preference } }(function() { - XNAT.plugin.jupyterhub.preferences.getAll = async function(timeout = 1000) { + XNAT.plugin.jupyterhub.preferences.getAll = async function() { console.debug(`jupyterhub-preferences.js: XNAT.plugin.jupyterhub.preferences.getAll`); let url = XNAT.url.restUrl('/xapi/jupyterhub/preferences'); const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(url, { method: 'GET', headers: {'Content-Type': 'application/json'}, - timeout: timeout, }) if (!response.ok) { @@ -40,14 +39,14 @@ XNAT.plugin.jupyterhub.preferences = getObject(XNAT.plugin.jupyterhub.preference return await response.json(); } - XNAT.plugin.jupyterhub.preferences.get = async function(preference, timeout = 1000) { + XNAT.plugin.jupyterhub.preferences.get = async function(preference) { console.debug(`jupyterhub-preferences.js: XNAT.plugin.jupyterhub.preferences.get`); - let preferences = await XNAT.plugin.jupyterhub.preferences.getAll(timeout); + let preferences = await XNAT.plugin.jupyterhub.preferences.getAll(); return preferences[preference]; } - XNAT.plugin.jupyterhub.preferences.set = async function(preference, value, timeout = 1000) { + XNAT.plugin.jupyterhub.preferences.set = async function(preference, value) { console.debug(`jupyterhub-preferences.js: XNAT.plugin.jupyterhub.preferences.set`); let url = XNAT.url.restUrl(`/xapi/jupyterhub/preferences/${preference}`); @@ -55,7 +54,6 @@ XNAT.plugin.jupyterhub.preferences = getObject(XNAT.plugin.jupyterhub.preference method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(value), - timeout: timeout, }) if (!response.ok) { diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-user-auth.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-user-auth.js index 3fa7fe3..3d83b18 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-user-auth.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-user-auth.js @@ -9,6 +9,7 @@ XNAT.plugin = getObject(XNAT.plugin || {}); XNAT.plugin.jupyterhub = getObject(XNAT.plugin.jupyterhub || {}); XNAT.plugin.jupyterhub.users = getObject(XNAT.plugin.jupyterhub.users || {}); XNAT.plugin.jupyterhub.users.authorization = getObject(XNAT.plugin.jupyterhub.users.authorization || {}); +XNAT.plugin.jupyterhub.users.authorization.roles = getObject(XNAT.plugin.jupyterhub.users.authorization.roles || {}); (function (factory) { if (typeof define === 'function' && define.amd) { @@ -19,9 +20,10 @@ XNAT.plugin.jupyterhub.users.authorization = getObject(XNAT.plugin.jupyterhub.us return factory(); } }(function () { + + XNAT.plugin.jupyterhub.users.authorization.roles.jupyter = 'Jupyter'; - XNAT.plugin.jupyterhub.users.authorization.isAuthorized = async function (username = window.username, - timeout = 1000) { + XNAT.plugin.jupyterhub.users.authorization.isAuthorized = async function (username = window.username) { console.debug(`XNAT.plugin.jupyterhub.users.authorization.isAuthorized`); if (username === 'guest') { @@ -31,7 +33,6 @@ XNAT.plugin.jupyterhub.users.authorization = getObject(XNAT.plugin.jupyterhub.us const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users/${username}/roles`, { method: 'GET', headers: {'Content-Type': 'application/json'}, - timeout: timeout, }) if (!response.ok) { @@ -39,9 +40,8 @@ XNAT.plugin.jupyterhub.users.authorization = getObject(XNAT.plugin.jupyterhub.us } let roles = await response.json(); - roles = roles.map(role => role.toLowerCase()); - if (roles.contains('administrator') || roles.contains('jupyter')) { + if (roles.contains(XNAT.plugin.jupyterhub.users.authorization.roles.jupyter)) { return true; } @@ -49,13 +49,12 @@ XNAT.plugin.jupyterhub.users.authorization = getObject(XNAT.plugin.jupyterhub.us return preferences['allUsersCanStartJupyter']; } - XNAT.plugin.jupyterhub.users.authorization.getAuthorized = async function (timeout = 1000) { + XNAT.plugin.jupyterhub.users.authorization.getAuthorized = async function () { console.debug(`XNAT.plugin.jupyterhub.users.authorization.getAuthorized`); - const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users/roles/Jupyter`, { + const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users/roles/${XNAT.plugin.jupyterhub.users.authorization.roles.jupyter}`, { method: 'GET', headers: {'Content-Type': 'application/json'}, - timeout: timeout, }) if (!response.ok) { @@ -65,7 +64,7 @@ XNAT.plugin.jupyterhub.users.authorization = getObject(XNAT.plugin.jupyterhub.us return await response.json(); } - XNAT.plugin.jupyterhub.users.authorization.getUnauthorized = async function (timeout = 1000) { + XNAT.plugin.jupyterhub.users.authorization.getUnauthorized = async function () { console.debug(`XNAT.plugin.jupyterhub.users.authorization.getUnauthorized`); let authorizedUsers = await XNAT.plugin.jupyterhub.users.authorization.getAuthorized(); @@ -73,7 +72,6 @@ XNAT.plugin.jupyterhub.users.authorization = getObject(XNAT.plugin.jupyterhub.us const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users`, { method: 'GET', headers: {'Content-Type': 'application/json'}, - timeout: timeout, }) if (!response.ok) { @@ -85,14 +83,13 @@ XNAT.plugin.jupyterhub.users.authorization = getObject(XNAT.plugin.jupyterhub.us return allUsers.filter(user => !authorizedUsers.contains(user) && user !== 'jupyterhub' && user !== 'guest'); } - XNAT.plugin.jupyterhub.users.authorization.add = async function (username, timeout = 1000) { + XNAT.plugin.jupyterhub.users.authorization.add = async function (username) { console.debug(`XNAT.plugin.jupyterhub.users.authorization.add`); const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users/${username}/roles`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(['Jupyter']), - timeout: timeout, + body: JSON.stringify([XNAT.plugin.jupyterhub.users.authorization.roles.jupyter]), }) if (!response.ok) { @@ -100,14 +97,13 @@ XNAT.plugin.jupyterhub.users.authorization = getObject(XNAT.plugin.jupyterhub.us } } - XNAT.plugin.jupyterhub.users.authorization.remove = async function (username, timeout = 1000) { + XNAT.plugin.jupyterhub.users.authorization.remove = async function (username) { console.debug(`XNAT.plugin.jupyterhub.users.authorization.remove`); const response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users/${username}/roles`, { method: 'DELETE', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(['Jupyter']), - timeout: timeout, + body: JSON.stringify([XNAT.plugin.jupyterhub.users.authorization.roles.jupyter]), }) if (!response.ok) { @@ -245,9 +241,10 @@ XNAT.plugin.jupyterhub.users.authorization = getObject(XNAT.plugin.jupyterhub.us xmodal.closeAll(); XNAT.ui.dialog.closeAll(); }).then(() => { + // Table refresh is delayed to allow time for the user authorization to be added setTimeout(() => { XNAT.plugin.jupyterhub.users.authorization.refreshTable(containerId); - }, 300); + }, 500); }) } }, diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java index f181dfc..d12f669 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java @@ -30,13 +30,12 @@ public class JupyterUserAuthorizationTest { @Autowired private JupyterHubPreferences mockJupyterHubPreferences; private UserI user; - private String username; @Before public void before() { // Mock the user user = mock(UserI.class); - username = "user"; + String username = "user"; when(user.getUsername()).thenReturn(username); } @@ -78,7 +77,7 @@ public void testJupyter_JupyterUsers() { boolean check = jupyterUserAuthorization.checkJupyter(user); // Verify - assertTrue("Jupyter All User preference is enabled. This should allow all.", check); + assertTrue("Jupyter role is enabled for the user. This should allow access.", check); } @Test @@ -92,7 +91,7 @@ public void testJupyter_Admin() { boolean check = jupyterUserAuthorization.checkJupyter(user); // Verify - assertTrue("Jupyter All User preference is enabled. This should allow all.", check); + assertFalse("Admin users are not allowed to start Jupyter. They should have the juptyer role.", check); } @Test From f83868da739c703142537277b612477daf494190 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Thu, 6 Apr 2023 16:27:53 -0500 Subject: [PATCH 12/49] JHP-41: Check XnatAppInfo isInitialized before creating JH user --- .../initialize/JupyterHubUserInitializer.java | 11 ++++++++++- .../config/JupyterHubUserInitializerConfig.java | 7 +++++-- .../plugins/jupyterhub/config/MockConfig.java | 6 ++++++ .../initialize/JupyterHubUserInitializerTest.java | 14 ++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java index 42f5b81..c0afb4a 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java @@ -10,6 +10,7 @@ import org.nrg.xft.security.UserI; import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -24,14 +25,17 @@ public class JupyterHubUserInitializer extends AbstractInitializingTask { private final UserManagementServiceI userManagementService; private final RoleHolder roleHolder; private final XFTManagerHelper xftManagerHelper; + private final XnatAppInfo appInfo; @Autowired public JupyterHubUserInitializer(final UserManagementServiceI userManagementService, final RoleHolder roleHolder, - final XFTManagerHelper xftManagerHelper) { + final XFTManagerHelper xftManagerHelper, + final XnatAppInfo appInfo) { this.userManagementService = userManagementService; this.roleHolder = roleHolder; this.xftManagerHelper = xftManagerHelper; + this.appInfo = appInfo; } @Override @@ -53,6 +57,11 @@ protected void callImpl() throws InitializingTaskException { throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); } + if (!appInfo.isInitialized()) { + log.debug("XNAT not initialized, deferring execution."); + throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); + } + if (userManagementService.exists("jupyterhub")) { log.debug("JupyterHub user already exists."); return; diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubUserInitializerConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubUserInitializerConfig.java index 9920d81..84f3a58 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubUserInitializerConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubUserInitializerConfig.java @@ -2,6 +2,7 @@ import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnatx.plugins.jupyterhub.initialize.JupyterHubUserInitializer; import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; import org.springframework.context.annotation.Bean; @@ -15,10 +16,12 @@ public class JupyterHubUserInitializerConfig { @Bean public JupyterHubUserInitializer defaultJupyterHubUserInitializer(final UserManagementServiceI mockUserManagementService, final RoleHolder mockRoleHolder, - final XFTManagerHelper mockXFTManagerHelper) { + final XFTManagerHelper mockXFTManagerHelper, + final XnatAppInfo mockXnatAppInfo) { return new JupyterHubUserInitializer(mockUserManagementService, mockRoleHolder, - mockXFTManagerHelper); + mockXFTManagerHelper, + mockXnatAppInfo); } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java index bcafcae..1318e56 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java @@ -9,6 +9,7 @@ import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.*; import org.nrg.xdat.services.AliasTokenService; +import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnat.tracking.services.EventTrackingDataHibernateService; import org.nrg.xnatx.plugins.jupyterhub.client.JupyterHubClient; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; @@ -144,4 +145,9 @@ public EventTrackingDataHibernateService mockEventTrackingDataHibernateService() public SerializerService mockSerializerService() { return Mockito.mock(SerializerService.class); } + + @Bean + public XnatAppInfo mockXnatAppInfo() { + return Mockito.mock(XnatAppInfo.class); + } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java index 1d3282a..738c3d0 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java @@ -9,6 +9,7 @@ import org.nrg.xft.event.EventDetails; import org.nrg.xft.security.UserI; import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubUserInitializerConfig; import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; import org.springframework.beans.factory.annotation.Autowired; @@ -25,6 +26,7 @@ public class JupyterHubUserInitializerTest { @Autowired private JupyterHubUserInitializer jupyterHubUserInitializer; @Autowired private UserManagementServiceI mockUserManagementService; @Autowired private XFTManagerHelper mockXFTManagerHelper; + @Autowired private XnatAppInfo mockXnatAppInfo; @Autowired private RoleServiceI mockRoleService; private final String username = "jupyterhub"; @@ -45,11 +47,22 @@ public void test_XFTManagerNotInitialized() throws Exception { jupyterHubUserInitializer.callImpl(); } + @Test(expected = InitializingTaskException.class) + public void test_AppNotInitialized() throws Exception { + // XFT initialized but app not initialized + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(false); + + // Should throw InitializingTaskException + jupyterHubUserInitializer.callImpl(); + } + @Test public void test_JHUserAlreadyExists() throws Exception { // Setup // XFT initialized and user already exists when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); when(mockUserManagementService.exists(username)).thenReturn(true); // Test @@ -121,6 +134,7 @@ public void test_FailedAddUserRole_Exception() throws Exception { public void test_UserCreated() throws Exception { // Setup when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); when(mockUserManagementService.exists(username)).thenReturn(false); UserI mockUser = mock(UserI.class); when(mockUserManagementService.createUser()).thenReturn(mockUser); From 9be5ff4b121c4d511f733cd40049603816f1a054 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Fri, 12 May 2023 15:52:49 -0500 Subject: [PATCH 13/49] JHP-20: Replaces Profiles concept with ComputeSpecs and Hardware --- build.gradle | 5 + .../config/JobTemplatesConfig.java | 12 + .../entities/ComputeSpecConfigEntity.java | 144 +++ .../entities/ComputeSpecEntity.java | 146 +++ .../ComputeSpecHardwareOptionsEntity.java | 114 ++ .../entities/ComputeSpecScopeEntity.java | 113 ++ .../entities/ConstraintConfigEntity.java | 101 ++ .../entities/ConstraintEntity.java | 85 ++ .../entities/ConstraintScopeEntity.java | 115 ++ .../entities/EnvironmentVariableEntity.java | 53 + .../entities/GenericResourceEntity.java | 53 + .../entities/HardwareConfigEntity.java | 111 ++ .../entities/HardwareConstraintEntity.java | 79 ++ .../jobtemplates/entities/HardwareEntity.java | 184 +++ .../entities/HardwareScopeEntity.java | 116 ++ .../jobtemplates/entities/MountEntity.java | 75 ++ .../nrg/jobtemplates/models/ComputeSpec.java | 20 + .../models/ComputeSpecConfig.java | 30 + .../models/ComputeSpecHardwareOptions.java | 19 + .../jobtemplates/models/ComputeSpecScope.java | 21 + .../nrg/jobtemplates/models/Constraint.java | 54 + .../jobtemplates/models/ConstraintConfig.java | 22 + .../jobtemplates/models/ConstraintScope.java | 21 + .../models/EnvironmentVariable.java | 17 + .../jobtemplates/models/GenericResource.java | 17 + .../org/nrg/jobtemplates/models/Hardware.java | 26 + .../jobtemplates/models/HardwareConfig.java | 22 + .../jobtemplates/models/HardwareScope.java | 21 + .../nrg/jobtemplates/models/JobTemplate.java | 20 + .../org/nrg/jobtemplates/models/Mount.java | 19 + .../repositories/ComputeSpecConfigDao.java | 112 ++ .../repositories/ConstraintConfigDao.java | 68 ++ .../repositories/HardwareConfigDao.java | 82 ++ .../rest/ComputeSpecConfigsApi.java | 129 ++ .../rest/ConstraintConfigsApi.java | 115 ++ .../jobtemplates/rest/HardwareConfigsApi.java | 112 ++ .../ComputeSpecConfigEntityService.java | 15 + .../services/ComputeSpecConfigService.java | 22 + .../ConstraintConfigEntityService.java | 8 + .../services/ConstraintConfigService.java | 18 + .../services/HardwareConfigEntityService.java | 8 + .../services/HardwareConfigService.java | 19 + .../services/JobTemplateService.java | 10 + .../impl/DefaultComputeSpecConfigService.java | 397 ++++++ .../impl/DefaultConstraintConfigService.java | 193 +++ .../impl/DefaultHardwareConfigService.java | 227 ++++ .../impl/DefaultJobTemplateService.java | 107 ++ ...bernateComputeSpecConfigEntityService.java | 77 ++ ...ibernateConstraintConfigEntityService.java | 14 + .../HibernateHardwareConfigEntityService.java | 14 + .../plugins/jupyterhub/JupyterHubPlugin.java | 9 +- .../jupyterhub/entities/ProfileEntity.java | 58 - ...HubEnvironmentsAndHardwareInitializer.java | 249 ++++ .../JupyterHubProfileInitializer.java | 95 -- .../plugins/jupyterhub/models/Profile.java | 31 - .../jupyterhub/models/ServerStartRequest.java | 28 + .../jupyterhub/repositories/ProfileDao.java | 12 - .../jupyterhub/rest/JupyterHubApi.java | 30 +- .../rest/JupyterHubProfilesApi.java | 120 -- .../services/JupyterHubService.java | 4 +- .../services/ProfileEntityService.java | 8 - .../jupyterhub/services/ProfileService.java | 18 - .../services/UserOptionsService.java | 2 +- .../impl/DefaultJupyterHubService.java | 131 +- .../services/impl/DefaultProfileService.java | 162 --- .../impl/DefaultUserOptionsService.java | 108 +- .../impl/HibernateProfileEntityService.java | 18 - .../jobTemplates/compute-spec-configs.js | 1014 +++++++++++++++ .../plugin/jobTemplates/constraint-configs.js | 612 ++++++++++ .../plugin/jobTemplates/hardware-configs.js | 1088 +++++++++++++++++ .../plugin/jupyterhub/jupyterhub-profiles.js | 645 ---------- .../plugin/jupyterhub/jupyterhub-servers.js | 236 ++-- .../plugin/jupyterhub/jupyterhub-users.js | 34 +- .../screens/topBar/Jupyter/Default.vm | 5 +- .../spawner/jupyterhub/site-settings.yaml | 81 +- src/main/resources/jupyterhub-logback.xml | 13 + .../config/ComputeSpecConfigsApiConfig.java} | 17 +- .../ConstraintConfigsApiTestConfig.java | 46 + ...ultComputeSpecConfigServiceTestConfig.java | 23 + ...aultConstraintConfigServiceTestConfig.java | 20 + ...efaultHardwareConfigServiceTestConfig.java | 23 + .../DefaultJobTemplateServiceTestConfig.java | 26 + .../config/HardwareConfigsApiConfig.java | 47 + ...puteSpecConfigEntityServiceTestConfig.java | 79 ++ .../jobtemplates/config/HibernateConfig.java | 79 ++ ...nstraintConfigEntityServiceTestConfig.java | 61 + .../config/HibernateEntityServicesConfig.java | 60 + ...HardwareConfigEntityServiceTestConfig.java | 61 + .../nrg/jobtemplates/config/MockConfig.java | 106 ++ .../config/ObjectMapperConfig.java | 29 + .../config/RestApiTestConfig.java | 86 ++ .../jobtemplates/models/ConstraintTest.java | 34 + .../rest/ComputeSpecConfigsApiTest.java | 232 ++++ .../rest/ConstraintConfigsApiTest.java | 206 ++++ .../rest/HardwareConfigsApiTest.java | 201 +++ .../DefaultComputeSpecConfigServiceTest.java | 735 +++++++++++ .../DefaultConstraintConfigServiceTest.java | 274 +++++ .../DefaultHardwareConfigServiceTest.java | 471 +++++++ .../impl/DefaultJobTemplateServiceTest.java | 198 +++ ...ateComputeSpecConfigEntityServiceTest.java | 505 ++++++++ ...nateConstraintConfigEntityServiceTest.java | 90 ++ ...ernateHardwareConfigEntityServiceTest.java | 23 + .../nrg/jobtemplates/utils/TestingUtils.java | 15 + .../DefaultJupyterHubServiceConfig.java | 6 +- .../config/DefaultProfileServiceConfig.java | 18 - .../DefaultUserOptionsServiceConfig.java | 6 +- ...mentsAndHardwareInitializerTestConfig.java | 29 + .../JupyterHubProfileInitializerConfig.java | 20 - .../plugins/jupyterhub/config/MockConfig.java | 28 +- ...nvironmentsAndHardwareInitializerTest.java | 127 ++ .../JupyterHubProfileInitializerTest.java | 76 -- .../JupyterHubUserInitializerTest.java | 6 + .../jupyterhub/rest/JupyterHubApiTest.java | 79 +- .../rest/JupyterHubProfilesApiTest.java | 318 ----- .../impl/DefaultJupyterHubServiceTest.java | 116 +- .../impl/DefaultProfileServiceTest.java | 285 ----- .../impl/DefaultUserOptionsServiceTest.java | 170 ++- 117 files changed, 11200 insertions(+), 2224 deletions(-) create mode 100644 src/main/java/org/nrg/jobtemplates/config/JobTemplatesConfig.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/ComputeSpecConfigEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/ComputeSpecEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/ComputeSpecHardwareOptionsEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/ComputeSpecScopeEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/ConstraintConfigEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/ConstraintEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/ConstraintScopeEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/EnvironmentVariableEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/GenericResourceEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/HardwareConfigEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/HardwareConstraintEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/HardwareEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/HardwareScopeEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/entities/MountEntity.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/ComputeSpec.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/ComputeSpecConfig.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/ComputeSpecHardwareOptions.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/ComputeSpecScope.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/Constraint.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/ConstraintConfig.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/ConstraintScope.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/EnvironmentVariable.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/GenericResource.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/Hardware.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/HardwareConfig.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/HardwareScope.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/JobTemplate.java create mode 100644 src/main/java/org/nrg/jobtemplates/models/Mount.java create mode 100644 src/main/java/org/nrg/jobtemplates/repositories/ComputeSpecConfigDao.java create mode 100644 src/main/java/org/nrg/jobtemplates/repositories/ConstraintConfigDao.java create mode 100644 src/main/java/org/nrg/jobtemplates/repositories/HardwareConfigDao.java create mode 100644 src/main/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApi.java create mode 100644 src/main/java/org/nrg/jobtemplates/rest/ConstraintConfigsApi.java create mode 100644 src/main/java/org/nrg/jobtemplates/rest/HardwareConfigsApi.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigEntityService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/ConstraintConfigEntityService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/ConstraintConfigService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/HardwareConfigEntityService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/HardwareConfigService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/JobTemplateService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityService.java create mode 100644 src/main/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityService.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/ProfileEntity.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Profile.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/ProfileDao.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApi.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileEntityService.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileService.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileService.java delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateProfileEntityService.java create mode 100644 src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/compute-spec-configs.js create mode 100644 src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/constraint-configs.js create mode 100644 src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/hardware-configs.js delete mode 100644 src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-profiles.js rename src/test/java/org/nrg/{xnatx/plugins/jupyterhub/config/JupyterHubProfilesApiConfig.java => jobtemplates/config/ComputeSpecConfigsApiConfig.java} (74%) create mode 100644 src/test/java/org/nrg/jobtemplates/config/ConstraintConfigsApiTestConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/DefaultComputeSpecConfigServiceTestConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/DefaultConstraintConfigServiceTestConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/DefaultHardwareConfigServiceTestConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/DefaultJobTemplateServiceTestConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/HardwareConfigsApiConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/HibernateComputeSpecConfigEntityServiceTestConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/HibernateConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/HibernateConstraintConfigEntityServiceTestConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/HibernateEntityServicesConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/HibernateHardwareConfigEntityServiceTestConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/MockConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/ObjectMapperConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/config/RestApiTestConfig.java create mode 100644 src/test/java/org/nrg/jobtemplates/models/ConstraintTest.java create mode 100644 src/test/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApiTest.java create mode 100644 src/test/java/org/nrg/jobtemplates/rest/ConstraintConfigsApiTest.java create mode 100644 src/test/java/org/nrg/jobtemplates/rest/HardwareConfigsApiTest.java create mode 100644 src/test/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigServiceTest.java create mode 100644 src/test/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigServiceTest.java create mode 100644 src/test/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigServiceTest.java create mode 100644 src/test/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateServiceTest.java create mode 100644 src/test/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityServiceTest.java create mode 100644 src/test/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityServiceTest.java create mode 100644 src/test/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityServiceTest.java create mode 100644 src/test/java/org/nrg/jobtemplates/utils/TestingUtils.java delete mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultProfileServiceConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java delete mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfileInitializerConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java delete mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializerTest.java delete mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApiTest.java delete mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileServiceTest.java diff --git a/build.gradle b/build.gradle index 7d77fa8..80ea344 100644 --- a/build.gradle +++ b/build.gradle @@ -41,10 +41,15 @@ dependencies { implementation 'org.projectlombok:lombok:1.18.24' testImplementation "com.fasterxml.jackson.datatype:jackson-datatype-guava" testImplementation "junit:junit" + testImplementation "com.h2database:h2" + testImplementation "org.hamcrest:hamcrest-library" testImplementation 'org.mockito:mockito-core:4.8.0' testImplementation "org.springframework:spring-test" testImplementation "org.springframework.security:spring-security-test" testImplementation "org.springframework.security:spring-security-config" + testImplementation "org.springframework:spring-jdbc" + testImplementation "org.springframework:spring-orm" + testImplementation "org.apache.commons:commons-dbcp2" testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' } diff --git a/src/main/java/org/nrg/jobtemplates/config/JobTemplatesConfig.java b/src/main/java/org/nrg/jobtemplates/config/JobTemplatesConfig.java new file mode 100644 index 0000000..96a8854 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/config/JobTemplatesConfig.java @@ -0,0 +1,12 @@ +package org.nrg.jobtemplates.config; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan({"org.nrg.jobtemplates.services.impl", + "org.nrg.jobtemplates.services", + "org.nrg.jobtemplates.rest", + "org.nrg.jobtemplates.repositories"}) +public class JobTemplatesConfig { +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecConfigEntity.java b/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecConfigEntity.java new file mode 100644 index 0000000..0f66407 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecConfigEntity.java @@ -0,0 +1,144 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.framework.constants.Scope; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; +import org.nrg.jobtemplates.models.ComputeSpecConfig; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class ComputeSpecConfigEntity extends AbstractHibernateEntity { + + private Set configTypes; + + private ComputeSpecEntity computeSpec; + private Map scopes; + private ComputeSpecHardwareOptionsEntity hardwareOptions; + + @ElementCollection + public Set getConfigTypes() { + if (configTypes == null) { + configTypes = new HashSet<>(); + } + + return configTypes; + } + + public void setConfigTypes(Set configTypes) { + this.configTypes = configTypes; + } + + @OneToOne(mappedBy = "computeSpecConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public ComputeSpecEntity getComputeSpec() { + return computeSpec; + } + + public void setComputeSpec(ComputeSpecEntity computeSpec) { + computeSpec.setComputeSpecConfig(this); + this.computeSpec = computeSpec; + } + + @OneToMany(mappedBy = "computeSpecConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public Map getScopes() { + return scopes; + } + + public void setScopes(Map scopes) { + this.scopes = scopes; + } + + @OneToOne(mappedBy = "computeSpecConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public ComputeSpecHardwareOptionsEntity getHardwareOptions() { + return hardwareOptions; + } + + public void setHardwareOptions(ComputeSpecHardwareOptionsEntity hardwareOptions) { + this.hardwareOptions = hardwareOptions; + } + + /** + * Creates a new entity from the pojo. + * @param pojo The pojo to create the entity from + * @return The newly created entity + */ + public static ComputeSpecConfigEntity fromPojo(final ComputeSpecConfig pojo) { + final ComputeSpecConfigEntity entity = new ComputeSpecConfigEntity(); + entity.update(pojo); + return entity; + } + + /** + * Creates a new pojo from the entity. + * @return The pojo created from the entity + */ + public ComputeSpecConfig toPojo() { + return ComputeSpecConfig.builder() + .id(getId()) + .configTypes(getConfigTypes() + .stream() + .map(ComputeSpecConfig.ConfigType::valueOf) + .collect(Collectors.toSet())) + .computeSpec(getComputeSpec().toPojo()) + .scopes(getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toPojo()))) + .hardwareOptions(getHardwareOptions().toPojo()) + .build(); + } + + /** + * Updates the entity with the values from the pojo. Does not update the hardwareConfigs, since that is a + * many-to-many relationship and needs to be handled separately. + * @param pojo The pojo to update the entity with + */ + public void update(final ComputeSpecConfig pojo) { + setConfigTypes(pojo.getConfigTypes() + .stream() + .map(Enum::name) + .collect(Collectors.toSet())); + + if (getComputeSpec() == null) { + // This is a new entity, so we need to create the computeSpec entity + setComputeSpec(ComputeSpecEntity.fromPojo(pojo.getComputeSpec())); + } else { + // This is an existing entity, so we need to update the computeSpec entity + getComputeSpec().update(pojo.getComputeSpec()); + } + + if (getScopes() == null) { + // This is a new entity, so we need to create the scopes entity + setScopes(pojo.getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> ComputeSpecScopeEntity.fromPojo(e.getValue())))); + } else { + // This is an existing entity, so we need to update the scopes entities + getScopes().forEach((key, value) -> value.update(pojo.getScopes().get(key))); + } + + // Set the computeSpecConfig on the scopes + getScopes().values().forEach(s -> s.setComputeSpecConfig(this)); + + if (getHardwareOptions() == null) { + // This is a new entity, so we need to create the hardwareOptions entity + ComputeSpecHardwareOptionsEntity computeSpecHardwareOptionsEntity = ComputeSpecHardwareOptionsEntity.fromPojo(pojo.getHardwareOptions()); + setHardwareOptions(computeSpecHardwareOptionsEntity); + computeSpecHardwareOptionsEntity.setComputeSpecConfig(this); + } else { + // This is an existing entity, so we need to update the hardwareOptions entity + getHardwareOptions().update(pojo.getHardwareOptions()); + } + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecEntity.java b/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecEntity.java new file mode 100644 index 0000000..a05f589 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecEntity.java @@ -0,0 +1,146 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; +import org.nrg.jobtemplates.models.ComputeSpec; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Entity +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"name"})}) +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class ComputeSpecEntity extends AbstractHibernateEntity { + + private String name; + private String image; + private String command; + + private List environmentVariables; + private List mounts; + + @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeSpecConfigEntity computeSpecConfig; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + @ElementCollection + public List getEnvironmentVariables() { + if (environmentVariables == null) { + environmentVariables = new ArrayList<>(); + } + + return environmentVariables; + } + + public void setEnvironmentVariables(List environmentVariables) { + this.environmentVariables = environmentVariables; + } + + @ElementCollection + public List getMounts() { + if (mounts == null) { + mounts = new ArrayList<>(); + } + + return mounts; + } + + public void setMounts(List mounts) { + this.mounts = mounts; + } + + @OneToOne + public ComputeSpecConfigEntity getComputeSpecConfig() { + return computeSpecConfig; + } + + public void setComputeSpecConfig(ComputeSpecConfigEntity computeSpecConfig) { + this.computeSpecConfig = computeSpecConfig; + } + + /** + * Creates a new entity from the given pojo. + * @param pojo The pojo to convert. + * @return The entity created from the pojo. + */ + public static ComputeSpecEntity fromPojo(ComputeSpec pojo) { + final ComputeSpecEntity entity = new ComputeSpecEntity(); + entity.update(pojo); + return entity; + } + + /** + * Converts this entity to a pojo. + * @return The pojo created from this entity. + */ + public ComputeSpec toPojo() { + return ComputeSpec.builder() + .name(getName()) + .image(getImage()) + .command(getCommand()) + .environmentVariables(getEnvironmentVariables().stream().map(EnvironmentVariableEntity::toPojo).collect(Collectors.toList())) + .mounts(getMounts().stream().map(MountEntity::toPojo).collect(Collectors.toList())) + .build(); + } + + /** + * Updates this entity from the given pojo. + * @param pojo The pojo to update from. + */ + public void update(ComputeSpec pojo) { + setName(pojo.getName()); + setImage(pojo.getImage()); + setCommand(pojo.getCommand()); + + // Clear the existing environment variables + getEnvironmentVariables().clear(); + + // Add the new environment variables + if (pojo.getEnvironmentVariables() != null) { + getEnvironmentVariables().addAll(pojo.getEnvironmentVariables() + .stream() + .map(EnvironmentVariableEntity::fromPojo) + .collect(Collectors.toList())); + } + + // Clear the existing mounts + getMounts().clear(); + + // Add the new mounts + if (pojo.getMounts() != null) { + getMounts().addAll(pojo.getMounts() + .stream() + .map(MountEntity::fromPojo) + .collect(Collectors.toList())); + } + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecHardwareOptionsEntity.java b/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecHardwareOptionsEntity.java new file mode 100644 index 0000000..2f25db9 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecHardwareOptionsEntity.java @@ -0,0 +1,114 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.jobtemplates.models.ComputeSpecHardwareOptions; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode +public class ComputeSpecHardwareOptionsEntity { + + private long id; + + @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeSpecConfigEntity computeSpecConfig; + + private boolean allowAllHardware; + private Set hardwareConfigs; + + @Id + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @OneToOne + @MapsId + public ComputeSpecConfigEntity getComputeSpecConfig() { + return computeSpecConfig; + } + + public void setComputeSpecConfig(ComputeSpecConfigEntity computeSpecConfig) { + this.computeSpecConfig = computeSpecConfig; + } + + public boolean isAllowAllHardware() { + return allowAllHardware; + } + + public void setAllowAllHardware(boolean allowAllHardware) { + this.allowAllHardware = allowAllHardware; + } + + @ManyToMany + @JoinTable(name = "compute_spec_hardware_options_hardware_config", + joinColumns = @JoinColumn(name = "compute_spec_hardware_options_id"), + inverseJoinColumns = @JoinColumn(name = "hardware_config_id")) + public Set getHardwareConfigs() { + return hardwareConfigs; + } + + public void setHardwareConfigs(Set hardwareConfigs) { + this.hardwareConfigs = hardwareConfigs; + } + + public void addHardwareConfig(HardwareConfigEntity hardwareConfig) { + if (hardwareConfigs == null) { + hardwareConfigs = new HashSet<>(); + } + + hardwareConfigs.add(hardwareConfig); + } + + public void removeHardwareConfig(HardwareConfigEntity hardwareConfig) { + if (hardwareConfigs != null) { + hardwareConfigs.remove(hardwareConfig); + } + } + + /** + * Creates a new entity from the given pojo. + * @param pojo The pojo to create the entity from. + * @return The newly created entity. + */ + public static ComputeSpecHardwareOptionsEntity fromPojo(final ComputeSpecHardwareOptions pojo) { + final ComputeSpecHardwareOptionsEntity entity = new ComputeSpecHardwareOptionsEntity(); + entity.update(pojo); + return entity; + } + + /** + * Creates a new pojo from the given entity. + * @return The newly created pojo. + */ + public ComputeSpecHardwareOptions toPojo() { + return ComputeSpecHardwareOptions.builder() + .allowAllHardware(allowAllHardware) + .hardwareConfigs(hardwareConfigs.stream().map(HardwareConfigEntity::toPojo).collect(Collectors.toSet())) + .build(); + } + + /** + * Updates the entity with the values from the given pojo. + * @param pojo The pojo to update the entity with. + */ + public void update(final ComputeSpecHardwareOptions pojo) { + setAllowAllHardware(pojo.isAllowAllHardware()); + + if (hardwareConfigs == null) { + hardwareConfigs = new HashSet<>(); + } + + // Updating the hardware configs is handled separately by the add/remove hardware config methods + } +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecScopeEntity.java b/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecScopeEntity.java new file mode 100644 index 0000000..476ea4e --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecScopeEntity.java @@ -0,0 +1,113 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.framework.constants.Scope; +import org.nrg.jobtemplates.models.ComputeSpecScope; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode +public class ComputeSpecScopeEntity { + + private long id; + private String scope; + private boolean enabled; + private Set ids; + + @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeSpecConfigEntity computeSpecConfig; + + @Id + @GeneratedValue + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @ElementCollection + public Set getIds() { + if (ids == null) { + ids = new HashSet<>(); + } + + return ids; + } + + public void setIds(Set ids) { + this.ids = ids; + } + + @ManyToOne + public ComputeSpecConfigEntity getComputeSpecConfig() { + return computeSpecConfig; + } + + public void setComputeSpecConfig(ComputeSpecConfigEntity computeSpecConfig) { + this.computeSpecConfig = computeSpecConfig; + } + + /** + * Updates this entity with the values from the given pojo. + * @param pojo The pojo to update from + */ + public void update(ComputeSpecScope pojo) { + setScope(pojo.getScope().name()); + setEnabled(pojo.isEnabled()); + + getIds().clear(); + + // add new ids + if (pojo.getIds() != null && !pojo.getIds().isEmpty()) { + getIds().addAll(pojo.getIds()); + } + } + + /** + * Converts this entity to a pojo. + * @return The pojo + */ + public ComputeSpecScope toPojo() { + return ComputeSpecScope.builder() + .scope(Scope.valueOf(getScope())) + .enabled(isEnabled()) + .ids(getIds()) + .build(); + } + + /** + * Creates a new entity from the given pojo. + * @param pojo The pojo to create from + * @return The new entity + */ + public static ComputeSpecScopeEntity fromPojo(ComputeSpecScope pojo) { + final ComputeSpecScopeEntity entity = new ComputeSpecScopeEntity(); + entity.update(pojo); + return entity; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/ConstraintConfigEntity.java b/src/main/java/org/nrg/jobtemplates/entities/ConstraintConfigEntity.java new file mode 100644 index 0000000..2bb5d74 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/ConstraintConfigEntity.java @@ -0,0 +1,101 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.framework.constants.Scope; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; +import org.nrg.jobtemplates.models.ConstraintConfig; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import java.util.Map; +import java.util.stream.Collectors; + +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class ConstraintConfigEntity extends AbstractHibernateEntity { + + private ConstraintEntity constraint; + private Map scopes; + + @OneToOne(mappedBy = "constraintConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public ConstraintEntity getConstraint() { + return constraint; + } + + public void setConstraint(ConstraintEntity constraint) { + constraint.setConstraintConfig(this); + this.constraint = constraint; + } + + @OneToMany(mappedBy = "constraintConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public Map getScopes() { + return scopes; + } + + public void setScopes(Map scopes) { + this.scopes = scopes; + } + + /** + * This method is used to update the entity from the pojo. + * @param pojo The pojo to update from. + */ + public void update(final ConstraintConfig pojo) { + if (getConstraint() == null) { + // New entity + setConstraint(ConstraintEntity.fromPojo(pojo.getConstraint())); + } else { + // Existing entity + getConstraint().update(pojo.getConstraint()); + } + + getConstraint().setConstraintConfig(this); + + if (getScopes() == null) { + // This is a new entity, so we need to create the scope entities + setScopes(pojo.getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> ConstraintScopeEntity.fromPojo(e.getValue())))); + } else { + // This is an existing entity, so we need to update the scope entities + getScopes().forEach((key, value) -> value.update(pojo.getScopes().get(key))); + } + + // Set the constraint config on the scope entities + getScopes().forEach((key, value) -> value.setConstraintConfig(this)); + } + + /** + * This method is used to convert the entity to a pojo. + * @return The pojo. + */ + public ConstraintConfig toPojo() { + return ConstraintConfig.builder() + .id(getId()) + .constraint(getConstraint().toPojo()) + .scopes(getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toPojo()))) + .build(); + } + + /** + * This method is used to create a new entity from the pojo. + * @param pojo The pojo to create from. + * @return The new entity. + */ + public static ConstraintConfigEntity fromPojo(final ConstraintConfig pojo) { + final ConstraintConfigEntity entity = new ConstraintConfigEntity(); + entity.update(pojo); + return entity; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/ConstraintEntity.java b/src/main/java/org/nrg/jobtemplates/entities/ConstraintEntity.java new file mode 100644 index 0000000..b4f1f40 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/ConstraintEntity.java @@ -0,0 +1,85 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; +import org.nrg.jobtemplates.models.Constraint; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class ConstraintEntity extends AbstractHibernateEntity { + + private String key; + private Set constraintValues; // Different from model, values is a reserved word + private String operator; + + @ToString.Exclude @EqualsAndHashCode.Exclude private ConstraintConfigEntity constraintConfig; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @ElementCollection + public Set getConstraintValues() { + return constraintValues; + } + + public void setConstraintValues(Set constraintValues) { + if (constraintValues == null) { + constraintValues = new HashSet<>(); + } + + this.constraintValues = constraintValues; + } + + public String getOperator() { + return operator; + } + + public void setOperator(String operator) { + this.operator = operator; + } + + @OneToOne + @MapsId + public ConstraintConfigEntity getConstraintConfig() { + return constraintConfig; + } + + public void setConstraintConfig(ConstraintConfigEntity constraintConfig) { + this.constraintConfig = constraintConfig; + } + + public void update(final Constraint constraint) { + setKey(constraint.getKey()); + setConstraintValues(constraint.getValues()); + setOperator(constraint.getOperator().toString()); + setConstraintConfig(this.constraintConfig); + } + + public Constraint toPojo() { + return Constraint.builder() + .key(getKey()) + .values(getConstraintValues()) + .operator(Constraint.Operator.valueOf(getOperator())) + .build(); + } + + public static ConstraintEntity fromPojo(final Constraint constraint) { + final ConstraintEntity entity = new ConstraintEntity(); + entity.update(constraint); + return entity; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/ConstraintScopeEntity.java b/src/main/java/org/nrg/jobtemplates/entities/ConstraintScopeEntity.java new file mode 100644 index 0000000..a9cc1fd --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/ConstraintScopeEntity.java @@ -0,0 +1,115 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.framework.constants.Scope; +import org.nrg.jobtemplates.models.ConstraintScope; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode +public class ConstraintScopeEntity { + + private long id; + private String scope; + private boolean enabled; + private Set ids; + + @ToString.Exclude @EqualsAndHashCode.Exclude private ConstraintConfigEntity constraintConfig; + + @Id + @GeneratedValue + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @ElementCollection + public Set getIds() { + return ids; + } + + public void setIds(Set ids) { + this.ids = ids; + } + + @ManyToOne + public ConstraintConfigEntity getConstraintConfig() { + return constraintConfig; + } + + public void setConstraintConfig(ConstraintConfigEntity constraintConfig) { + this.constraintConfig = constraintConfig; + } + + /** + * Converts this entity to a pojo. + * @return The pojo. + */ + public ConstraintScope toPojo() { + return ConstraintScope.builder() + .scope(Scope.valueOf(getScope())) + .enabled(isEnabled()) + .ids(getIds()) + .build(); + } + + /** + * Updates this entity from the given pojo. + * @param pojo The pojo. + */ + public void update(final ConstraintScope pojo) { + setScope(pojo.getScope().name()); + setEnabled(pojo.isEnabled()); + + if (getIds() == null) { + // This is a new entity, so we need to initialize the collection + setIds(new HashSet<>()); + } else { + // This is an existing entity, so we need to clear the collection + getIds().clear(); + } + + // add new ids + if (pojo.getIds() != null) { + getIds().addAll(pojo.getIds()); + } + } + + /** + * Creates a new entity from the given pojo. + * @param pojo The pojo. + * @return The entity. + */ + public static ConstraintScopeEntity fromPojo(final ConstraintScope pojo) { + final ConstraintScopeEntity entity = new ConstraintScopeEntity(); + entity.update(pojo); + return entity; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/EnvironmentVariableEntity.java b/src/main/java/org/nrg/jobtemplates/entities/EnvironmentVariableEntity.java new file mode 100644 index 0000000..896b0c8 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/EnvironmentVariableEntity.java @@ -0,0 +1,53 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.jobtemplates.models.EnvironmentVariable; + +import javax.persistence.Embeddable; + +@Embeddable +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode +public class EnvironmentVariableEntity { + + private String key; + private String value; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public void update(EnvironmentVariable environmentVariable) { + setKey(environmentVariable.getKey()); + setValue(environmentVariable.getValue()); + } + + public EnvironmentVariable toPojo() { + return EnvironmentVariable.builder() + .key(key) + .value(value) + .build(); + } + + public static EnvironmentVariableEntity fromPojo(EnvironmentVariable environmentVariable) { + final EnvironmentVariableEntity entity = new EnvironmentVariableEntity(); + entity.update(environmentVariable); + return entity; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/GenericResourceEntity.java b/src/main/java/org/nrg/jobtemplates/entities/GenericResourceEntity.java new file mode 100644 index 0000000..bcd2109 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/GenericResourceEntity.java @@ -0,0 +1,53 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.jobtemplates.models.GenericResource; + +import javax.persistence.Embeddable; + +@Embeddable +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode +public class GenericResourceEntity { + + private String name; + private String value; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public void update(GenericResource genericResource) { + setName(genericResource.getName()); + setValue(genericResource.getValue()); + } + + public GenericResource toPojo() { + return GenericResource.builder() + .name(name) + .value(value) + .build(); + } + + public static GenericResourceEntity fromPojo(GenericResource genericResource) { + final GenericResourceEntity entity = new GenericResourceEntity(); + entity.update(genericResource); + return entity; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/HardwareConfigEntity.java b/src/main/java/org/nrg/jobtemplates/entities/HardwareConfigEntity.java new file mode 100644 index 0000000..ab27f0d --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/HardwareConfigEntity.java @@ -0,0 +1,111 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.framework.constants.Scope; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; +import org.nrg.jobtemplates.models.HardwareConfig; + +import javax.persistence.*; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class HardwareConfigEntity extends AbstractHibernateEntity { + + private HardwareEntity hardware; + private Map scopes; + + @ToString.Exclude @EqualsAndHashCode.Exclude private List computeSpecHardwareOptions; + + @OneToOne(mappedBy = "hardwareConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public HardwareEntity getHardware() { + return hardware; + } + + public void setHardware(HardwareEntity hardware) { + hardware.setHardwareConfig(this); + this.hardware = hardware; + } + + @OneToMany(mappedBy = "hardwareConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public Map getScopes() { + return scopes; + } + + public void setScopes(Map scopes) { + this.scopes = scopes; + } + + @ManyToMany(mappedBy = "hardwareConfigs") + public List getComputeSpecHardwareOptions() { + return computeSpecHardwareOptions; + } + + public void setComputeSpecHardwareOptions(List computeSpecHardwareOptions) { + this.computeSpecHardwareOptions = computeSpecHardwareOptions; + } + + /** + * Creates a new entity from the given pojo. + * @param pojo The pojo from which to create the entity. + * @return The newly created entity. + */ + public static HardwareConfigEntity fromPojo(final HardwareConfig pojo) { + final HardwareConfigEntity entity = new HardwareConfigEntity(); + entity.update(pojo); + return entity; + } + + /** + * Creates a new pojo from the given entity. + * @return The newly created pojo. + */ + public HardwareConfig toPojo() { + return HardwareConfig.builder() + .id(getId()) + .hardware(getHardware().toPojo()) + .scopes(getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toPojo()))) + .build(); + } + + /** + * Updates the entity with the values from the given pojo. + * @param pojo The pojo from which to update the entity. + */ + public void update(final HardwareConfig pojo) { + if (getHardware() == null) { + // This is a new entity, so we need to create the hardware entity + setHardware(HardwareEntity.fromPojo(pojo.getHardware())); + } else { + // This is an existing entity, so we need to update the hardware entity + getHardware().update(pojo.getHardware()); + } + + // Set the hardware config on the hardware entity + getHardware().setHardwareConfig(this); + + if (getScopes() == null) { + // This is a new entity, so we need to create the scope entities + setScopes(pojo.getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> HardwareScopeEntity.fromPojo(e.getValue())))); + } else { + // This is an existing entity, so we need to update the scope entities + getScopes().forEach((key, value) -> value.update(pojo.getScopes().get(key))); + } + + // Set the hardware config on the scope entities + getScopes().forEach((key, value) -> value.setHardwareConfig(this)); + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/HardwareConstraintEntity.java b/src/main/java/org/nrg/jobtemplates/entities/HardwareConstraintEntity.java new file mode 100644 index 0000000..a0875e3 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/HardwareConstraintEntity.java @@ -0,0 +1,79 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; +import org.nrg.jobtemplates.models.Constraint; + +import javax.persistence.*; +import java.util.Set; + +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class HardwareConstraintEntity extends AbstractHibernateEntity { + + private String key; + private Set constraintValues; // Different from model, values is a reserved word + private String operator; + + @ToString.Exclude @EqualsAndHashCode.Exclude private HardwareEntity hardware; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @ElementCollection(fetch = FetchType.EAGER) + public Set getConstraintValues() { + return constraintValues; + } + + public void setConstraintValues(Set constraintValues) { + this.constraintValues = constraintValues; + } + + public String getOperator() { + return operator; + } + + public void setOperator(String operator) { + this.operator = operator; + } + + @ManyToOne + @JoinColumn(name = "hardware_entity_id") + public HardwareEntity getHardware() { + return hardware; + } + + public void setHardware(HardwareEntity hardware) { + this.hardware = hardware; + } + + public void update(final Constraint constraint) { + setKey(constraint.getKey()); + setConstraintValues(constraint.getValues()); + setOperator(constraint.getOperator().toString()); + setHardware(this.hardware); + } + + public Constraint toPojo() { + return Constraint.builder() + .key(key) + .values(constraintValues) + .operator(Constraint.Operator.valueOf(operator)) + .build(); + } + + public static HardwareConstraintEntity fromPojo(final Constraint constraint) { + final HardwareConstraintEntity entity = new HardwareConstraintEntity(); + entity.update(constraint); + return entity; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/HardwareEntity.java b/src/main/java/org/nrg/jobtemplates/entities/HardwareEntity.java new file mode 100644 index 0000000..c8c31d4 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/HardwareEntity.java @@ -0,0 +1,184 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; +import org.nrg.jobtemplates.models.Hardware; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Entity +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"name"})}) +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class HardwareEntity extends AbstractHibernateEntity { + + private String name; + + private Double cpuReservation; + private Double cpuLimit; + private String memoryReservation; + private String memoryLimit; + + private List constraints; + private List environmentVariables; + private List genericResources; + + @ToString.Exclude @EqualsAndHashCode.Exclude private HardwareConfigEntity hardwareConfig; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Double getCpuReservation() { + return cpuReservation; + } + + public void setCpuReservation(Double cpuReservation) { + this.cpuReservation = cpuReservation; + } + + public Double getCpuLimit() { + return cpuLimit; + } + + public void setCpuLimit(Double cpuLimit) { + this.cpuLimit = cpuLimit; + } + + public String getMemoryReservation() { + return memoryReservation; + } + + public void setMemoryReservation(String memoryReservation) { + this.memoryReservation = memoryReservation; + } + + public String getMemoryLimit() { + return memoryLimit; + } + + public void setMemoryLimit(String memoryLimit) { + this.memoryLimit = memoryLimit; + } + + @OneToMany(mappedBy = "hardware", cascade = CascadeType.ALL, orphanRemoval = true) + public List getConstraints() { + if (constraints == null) { + constraints = new ArrayList<>(); + } + + return constraints; + } + + public void setConstraints(List constraints) { + this.constraints = constraints; + } + + @ElementCollection + public List getEnvironmentVariables() { + if (environmentVariables == null) { + environmentVariables = new ArrayList<>(); + } + + return environmentVariables; + } + + public void setEnvironmentVariables(List environmentVariables) { + this.environmentVariables = environmentVariables; + } + + @ElementCollection + public List getGenericResources() { + if (genericResources == null) { + genericResources = new ArrayList<>(); + } + + return genericResources; + } + + public void setGenericResources(List genericResources) { + this.genericResources = genericResources; + } + + @OneToOne + public HardwareConfigEntity getHardwareConfig() { + return hardwareConfig; + } + + public void setHardwareConfig(HardwareConfigEntity hardwareConfig) { + this.hardwareConfig = hardwareConfig; + } + + public static HardwareEntity fromPojo(Hardware pojo) { + HardwareEntity entity = new HardwareEntity(); + entity.update(pojo); + return entity; + } + + public Hardware toPojo() { + return Hardware.builder() + .name(getName()) + .cpuReservation(getCpuReservation()) + .cpuLimit(getCpuLimit()) + .memoryReservation(getMemoryReservation()) + .memoryLimit(getMemoryLimit()) + .constraints(getConstraints().stream().map(HardwareConstraintEntity::toPojo).collect(Collectors.toList())) + .environmentVariables(getEnvironmentVariables().stream().map(EnvironmentVariableEntity::toPojo).collect(Collectors.toList())) + .genericResources(getGenericResources().stream().map(GenericResourceEntity::toPojo).collect(Collectors.toList())) + .build(); + } + + public void update(Hardware pojo) { + setName(pojo.getName()); + setCpuReservation(pojo.getCpuReservation()); + setCpuLimit(pojo.getCpuLimit()); + setMemoryReservation(pojo.getMemoryReservation()); + setMemoryLimit(pojo.getMemoryLimit()); + + // remove old constraints, need to remove hardware reference from old constraints before clearing + getConstraints().forEach(c -> c.setHardware(null)); + getConstraints().clear(); + + // add new constraints + if (pojo.getConstraints() != null) { + getConstraints().addAll(pojo.getConstraints() + .stream() + .map(HardwareConstraintEntity::fromPojo) + .collect(Collectors.toList())); + getConstraints().forEach(c -> c.setHardware(this)); + } + + // remove old environment variables + getEnvironmentVariables().clear(); + + // add new environment variables + if (pojo.getEnvironmentVariables() != null) { + getEnvironmentVariables().addAll(pojo.getEnvironmentVariables() + .stream() + .map(EnvironmentVariableEntity::fromPojo) + .collect(Collectors.toList())); + } + + // remove old resources + getGenericResources().clear(); + + // add new resources + if (pojo.getGenericResources() != null) { + getGenericResources().addAll(pojo.getGenericResources() + .stream() + .map(GenericResourceEntity::fromPojo) + .collect(Collectors.toList())); + } + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/HardwareScopeEntity.java b/src/main/java/org/nrg/jobtemplates/entities/HardwareScopeEntity.java new file mode 100644 index 0000000..00af3ad --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/HardwareScopeEntity.java @@ -0,0 +1,116 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.framework.constants.Scope; +import org.nrg.jobtemplates.models.HardwareScope; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode +public class HardwareScopeEntity { + + private long id; + private String scope; + private boolean enabled; + private Set ids; + + @ToString.Exclude @EqualsAndHashCode.Exclude private HardwareConfigEntity hardwareConfig; + + @Id + @GeneratedValue + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public boolean isEnabled() { + return enabled; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @ElementCollection + public Set getIds() { + return ids; + } + + public void setIds(Set ids) { + this.ids = ids; + } + + @ManyToOne + public HardwareConfigEntity getHardwareConfig() { + return hardwareConfig; + } + + public void setHardwareConfig(HardwareConfigEntity hardwareConfig) { + this.hardwareConfig = hardwareConfig; + } + + /** + * Converts this entity to a pojo. + * @return The pojo. + */ + public HardwareScope toPojo() { + return HardwareScope.builder() + .scope(Scope.valueOf(getScope())) + .enabled(isEnabled()) + .ids(getIds()) + .build(); + } + + /** + * Updates this entity from the given pojo. + * @param pojo The pojo. + */ + public void update(final HardwareScope pojo) { + setScope(pojo.getScope().name()); + setEnabled(pojo.isEnabled()); + + if (getIds() == null) { + // This is a new entity, so we need to initialize the collection + setIds(new HashSet<>()); + } else { + // This is an existing entity, so we need to clear the collection + getIds().clear(); + } + + // add new ids + if (pojo.getIds() != null) { + getIds().addAll(pojo.getIds()); + } + } + + /** + * Creates a new entity from the given pojo. + * @param pojo The pojo. + * @return The entity. + */ + public static HardwareScopeEntity fromPojo(final HardwareScope pojo) { + final HardwareScopeEntity entity = new HardwareScopeEntity(); + entity.update(pojo); + return entity; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/MountEntity.java b/src/main/java/org/nrg/jobtemplates/entities/MountEntity.java new file mode 100644 index 0000000..25d6647 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/entities/MountEntity.java @@ -0,0 +1,75 @@ +package org.nrg.jobtemplates.entities; + +import lombok.*; +import org.nrg.jobtemplates.models.Mount; + +import javax.persistence.Embeddable; + +@Embeddable +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode +public class MountEntity { + + private String volumeName; + private String localPath; + private String containerPath; + private boolean readOnly; + + public String getVolumeName() { + return volumeName; + } + + public void setVolumeName(String volumeName) { + this.volumeName = volumeName; + } + + public String getLocalPath() { + return localPath; + } + + public void setLocalPath(String localPath) { + this.localPath = localPath; + } + + public String getContainerPath() { + return containerPath; + } + + public void setContainerPath(String containerPath) { + this.containerPath = containerPath; + } + + public boolean isReadOnly() { + return readOnly; + } + + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + public void update(Mount mount) { + setVolumeName(mount.getVolumeName()); + setLocalPath(mount.getLocalPath()); + setContainerPath(mount.getContainerPath()); + setReadOnly(mount.isReadOnly()); + } + + public Mount toPojo() { + return Mount.builder() + .volumeName(volumeName) + .localPath(localPath) + .containerPath(containerPath) + .readOnly(readOnly) + .build(); + } + + public static MountEntity fromPojo(Mount mount) { + final MountEntity entity = new MountEntity(); + entity.update(mount); + return entity; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/ComputeSpec.java b/src/main/java/org/nrg/jobtemplates/models/ComputeSpec.java new file mode 100644 index 0000000..15bcda1 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/ComputeSpec.java @@ -0,0 +1,20 @@ +package org.nrg.jobtemplates.models; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import java.util.List; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ComputeSpec { + + @ApiModelProperty(position = 0) private String name; + @ApiModelProperty(position = 1) private String image; + @ApiModelProperty(position = 2) private String command; + @ApiModelProperty(position = 3) private List environmentVariables; + @ApiModelProperty(position = 4) private List mounts; + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/ComputeSpecConfig.java b/src/main/java/org/nrg/jobtemplates/models/ComputeSpecConfig.java new file mode 100644 index 0000000..c10ac11 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/ComputeSpecConfig.java @@ -0,0 +1,30 @@ +package org.nrg.jobtemplates.models; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.nrg.framework.constants.Scope; + +import java.util.Map; +import java.util.Set; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ComputeSpecConfig { + + @ApiModelProperty(position = 0) private Long id; + @ApiModelProperty(position = 1) private Set configTypes; + @ApiModelProperty(position = 2) private ComputeSpec computeSpec; + @ApiModelProperty(position = 3) private Map scopes; + @ApiModelProperty(position = 4) private ComputeSpecHardwareOptions hardwareOptions; + + public enum ConfigType { + JUPYTERHUB, + CONTAINER_SERVICE, + GENERAL + } +} diff --git a/src/main/java/org/nrg/jobtemplates/models/ComputeSpecHardwareOptions.java b/src/main/java/org/nrg/jobtemplates/models/ComputeSpecHardwareOptions.java new file mode 100644 index 0000000..2c21f9a --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/ComputeSpecHardwareOptions.java @@ -0,0 +1,19 @@ +package org.nrg.jobtemplates.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ComputeSpecHardwareOptions { + + private boolean allowAllHardware; + private Set hardwareConfigs; + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/ComputeSpecScope.java b/src/main/java/org/nrg/jobtemplates/models/ComputeSpecScope.java new file mode 100644 index 0000000..146c414 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/ComputeSpecScope.java @@ -0,0 +1,21 @@ +package org.nrg.jobtemplates.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.nrg.framework.constants.Scope; + +import java.util.Set; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ComputeSpecScope { + + private Scope scope; + private boolean enabled; // For project and user scopes, this is whether the scope is enabled for all projects or users + private Set ids; // For project and user scopes, this is the set of project or user IDs that are enabled for this scope (if isEnabled is false) + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/Constraint.java b/src/main/java/org/nrg/jobtemplates/models/Constraint.java new file mode 100644 index 0000000..31f4e97 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/Constraint.java @@ -0,0 +1,54 @@ +package org.nrg.jobtemplates.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Constraint { + + public enum Operator { + IN, + NOT_IN, + } + + private String key; + private Set values; + private Operator operator; + + /** + * Convert to list of Docker constraint strings. Example: ["key==value1", "key==value2"] + * @return List of Docker constraint strings + */ + public List toList() { + List list = new ArrayList<>(); + + values.forEach((value) -> { + final String operatorString; + + switch (operator) { + case IN: + operatorString = "=="; + break; + case NOT_IN: + operatorString = "!="; + break; + default: + throw new RuntimeException("Unknown constraint operator: " + operator); + } + + list.add(key + operatorString + value); + }); + + return list; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/ConstraintConfig.java b/src/main/java/org/nrg/jobtemplates/models/ConstraintConfig.java new file mode 100644 index 0000000..66ea874 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/ConstraintConfig.java @@ -0,0 +1,22 @@ +package org.nrg.jobtemplates.models; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.nrg.framework.constants.Scope; + +import java.util.Map; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ConstraintConfig { + + @ApiModelProperty(position = 0) private Long id; + @ApiModelProperty(position = 1) private Constraint constraint; + @ApiModelProperty(position = 2) private Map scopes; + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/ConstraintScope.java b/src/main/java/org/nrg/jobtemplates/models/ConstraintScope.java new file mode 100644 index 0000000..9316dfd --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/ConstraintScope.java @@ -0,0 +1,21 @@ +package org.nrg.jobtemplates.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.nrg.framework.constants.Scope; + +import java.util.Set; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ConstraintScope { + + private Scope scope; + private boolean enabled; // For project and user scopes, this is whether the scope is enabled for all projects or users + private Set ids; // For project and user scopes, this is the set of project or user IDs that are enabled for this scope (if isEnabled is false) + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/EnvironmentVariable.java b/src/main/java/org/nrg/jobtemplates/models/EnvironmentVariable.java new file mode 100644 index 0000000..08f80e1 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/EnvironmentVariable.java @@ -0,0 +1,17 @@ +package org.nrg.jobtemplates.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class EnvironmentVariable { + + private String key; + private String value; + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/GenericResource.java b/src/main/java/org/nrg/jobtemplates/models/GenericResource.java new file mode 100644 index 0000000..7ac5a9b --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/GenericResource.java @@ -0,0 +1,17 @@ +package org.nrg.jobtemplates.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class GenericResource { + + private String name; + private String value; + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/Hardware.java b/src/main/java/org/nrg/jobtemplates/models/Hardware.java new file mode 100644 index 0000000..ddba1d1 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/Hardware.java @@ -0,0 +1,26 @@ +package org.nrg.jobtemplates.models; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Hardware { + + @ApiModelProperty(position = 0) private String name; + @ApiModelProperty(position = 1) private Double cpuLimit; + @ApiModelProperty(position = 2) private Double cpuReservation; + @ApiModelProperty(position = 3) private String memoryLimit; + @ApiModelProperty(position = 4) private String memoryReservation; + @ApiModelProperty(position = 5) private List constraints; + @ApiModelProperty(position = 6) private List environmentVariables; + @ApiModelProperty(position = 7) private List genericResources; + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/HardwareConfig.java b/src/main/java/org/nrg/jobtemplates/models/HardwareConfig.java new file mode 100644 index 0000000..e7c1c60 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/HardwareConfig.java @@ -0,0 +1,22 @@ +package org.nrg.jobtemplates.models; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.nrg.framework.constants.Scope; + +import java.util.Map; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class HardwareConfig { + + @ApiModelProperty(position = 0) private Long id; + @ApiModelProperty(position = 1) private Hardware hardware; + @ApiModelProperty(position = 2) private Map scopes; + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/HardwareScope.java b/src/main/java/org/nrg/jobtemplates/models/HardwareScope.java new file mode 100644 index 0000000..81a1934 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/HardwareScope.java @@ -0,0 +1,21 @@ +package org.nrg.jobtemplates.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.nrg.framework.constants.Scope; + +import java.util.Set; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class HardwareScope { + + private Scope scope; + private boolean enabled; // For project and user scopes, this is whether the scope is enabled for all projects or users + private Set ids; // For project and user scopes, this is the set of project or user IDs that are enabled for this scope (if isEnabled is false) + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/JobTemplate.java b/src/main/java/org/nrg/jobtemplates/models/JobTemplate.java new file mode 100644 index 0000000..ace6fdf --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/JobTemplate.java @@ -0,0 +1,20 @@ +package org.nrg.jobtemplates.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class JobTemplate { + + private ComputeSpec computeSpec; + private Hardware hardware; + private List constraints; + +} diff --git a/src/main/java/org/nrg/jobtemplates/models/Mount.java b/src/main/java/org/nrg/jobtemplates/models/Mount.java new file mode 100644 index 0000000..dedd5fc --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/models/Mount.java @@ -0,0 +1,19 @@ +package org.nrg.jobtemplates.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Mount { + + private String volumeName; + private String localPath; + private String containerPath; + private boolean readOnly; + +} diff --git a/src/main/java/org/nrg/jobtemplates/repositories/ComputeSpecConfigDao.java b/src/main/java/org/nrg/jobtemplates/repositories/ComputeSpecConfigDao.java new file mode 100644 index 0000000..67520ca --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/repositories/ComputeSpecConfigDao.java @@ -0,0 +1,112 @@ +package org.nrg.jobtemplates.repositories; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.Criteria; +import org.hibernate.Hibernate; +import org.hibernate.SessionFactory; +import org.hibernate.criterion.Restrictions; +import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; +import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; +import org.nrg.jobtemplates.entities.ComputeSpecScopeEntity; +import org.nrg.jobtemplates.entities.HardwareConfigEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import java.util.Collections; +import java.util.List; + +@Repository +@Slf4j +public class ComputeSpecConfigDao extends AbstractHibernateDAO { + + private final HardwareConfigDao hardwareConfigDao; + + // For testing + public ComputeSpecConfigDao(final SessionFactory sessionFactory, + final HardwareConfigDao hardwareConfigDao) { + super(sessionFactory); + this.hardwareConfigDao = hardwareConfigDao; + } + + @Autowired + public ComputeSpecConfigDao(HardwareConfigDao hardwareConfigDao) { + this.hardwareConfigDao = hardwareConfigDao; + } + + /** + * Initializes the entity, loading all collections and proxies. + * @param entity The entity to initialize. + */ + @Override + public void initialize(final ComputeSpecConfigEntity entity) { + if (entity == null) { + return; + } + + Hibernate.initialize(entity); + + Hibernate.initialize(entity.getConfigTypes()); + + Hibernate.initialize(entity.getComputeSpec()); + if (entity.getComputeSpec() != null) { + Hibernate.initialize(entity.getComputeSpec().getEnvironmentVariables()); + Hibernate.initialize(entity.getComputeSpec().getMounts()); + } + + Hibernate.initialize(entity.getScopes()); + if (entity.getScopes() != null) { + entity.getScopes().forEach((scope, computeSpecScopeEntity) -> { + initialize(computeSpecScopeEntity); + }); + } + + Hibernate.initialize(entity.getHardwareOptions()); + if (entity.getHardwareOptions() != null) { + Hibernate.initialize(entity.getHardwareOptions().getHardwareConfigs()); + + if (entity.getHardwareOptions().getHardwareConfigs() != null) { + for (HardwareConfigEntity hardwareConfig : entity.getHardwareOptions().getHardwareConfigs()) { + hardwareConfigDao.initialize(hardwareConfig); + } + } + } + } + + /** + * Initializes the entity, loading all collections and proxies. + * @param entity The entity to initialize. + */ + public void initialize(ComputeSpecScopeEntity entity) { + if (entity == null) { + return; + } + + Hibernate.initialize(entity); + Hibernate.initialize(entity.getIds()); + } + + /** + * Finds all compute spec configs that have the specified type. + * @param type The type to search for. + * @return The list of compute spec configs that have the specified type. + */ + public List findByType(String type) { + // Need to use a criteria query because the configTypes field is a collection. + Criteria criteria = getSession().createCriteria(ComputeSpecConfigEntity.class) + .createCriteria("configTypes") + .add(Restrictions.eq("elements", type)); + + List entities = criteria.list(); + + if (entities == null) { + return Collections.emptyList(); + } + + for (ComputeSpecConfigEntity entity : entities) { + initialize(entity); + } + + return entities; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/repositories/ConstraintConfigDao.java b/src/main/java/org/nrg/jobtemplates/repositories/ConstraintConfigDao.java new file mode 100644 index 0000000..966249c --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/repositories/ConstraintConfigDao.java @@ -0,0 +1,68 @@ +package org.nrg.jobtemplates.repositories; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.Hibernate; +import org.hibernate.SessionFactory; +import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; +import org.nrg.jobtemplates.entities.ConstraintConfigEntity; +import org.nrg.jobtemplates.entities.ConstraintEntity; +import org.nrg.jobtemplates.entities.ConstraintScopeEntity; +import org.springframework.stereotype.Repository; + +@Repository +@Slf4j +public class ConstraintConfigDao extends AbstractHibernateDAO { + + // For testing + public ConstraintConfigDao(final SessionFactory sessionFactory) { + super(sessionFactory); + } + + /** + * Initialize the constraint config entity. + * @param entity The entity to initialize. + */ + @Override + public void initialize(final ConstraintConfigEntity entity) { + if (entity == null) { + return; + } + + initialize(entity.getConstraint()); + + Hibernate.initialize(entity.getScopes()); + if (entity.getScopes() != null) { + entity.getScopes().forEach((scope, constraintScopeEntity) -> { + initialize(constraintScopeEntity); + }); + } + } + + /** + * Initialize the constraint entity. + * @param entity The entity to initialize. + */ + void initialize(final ConstraintEntity entity) { + if (entity == null) { + return; + } + + Hibernate.initialize(entity); + if (entity.getConstraintValues() != null) { + Hibernate.initialize(entity.getConstraintValues()); + } + } + + /** + * Initialize the constraint scope entity. + * @param entity The entity to initialize. + */ + public void initialize(ConstraintScopeEntity entity) { + if (entity == null) { + return; + } + + Hibernate.initialize(entity); + Hibernate.initialize(entity.getIds()); + } +} diff --git a/src/main/java/org/nrg/jobtemplates/repositories/HardwareConfigDao.java b/src/main/java/org/nrg/jobtemplates/repositories/HardwareConfigDao.java new file mode 100644 index 0000000..0cf67b2 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/repositories/HardwareConfigDao.java @@ -0,0 +1,82 @@ +package org.nrg.jobtemplates.repositories; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.Hibernate; +import org.hibernate.SessionFactory; +import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; +import org.nrg.jobtemplates.entities.*; +import org.springframework.stereotype.Repository; + +@Repository +@Slf4j +public class HardwareConfigDao extends AbstractHibernateDAO { + + // For testing + public HardwareConfigDao(final SessionFactory sessionFactory) { + super(sessionFactory); + } + + /** + * Initializes the entity, loading all collections and proxies. + * @param entity The entity to initialize. + */ + @Override + public void initialize(final HardwareConfigEntity entity) { + if (entity == null) { + return; + } + + Hibernate.initialize(entity); + + Hibernate.initialize(entity.getScopes()); + if (entity.getScopes() != null) { + entity.getScopes().forEach((scope, hardwareScopeEntity) -> { + initialize(hardwareScopeEntity); + }); + } + + Hibernate.initialize(entity.getHardware()); + if (entity.getHardware() != null) { + initialize(entity.getHardware()); + } + + if (entity.getComputeSpecHardwareOptions() != null) { + Hibernate.initialize(entity.getComputeSpecHardwareOptions()); + } + } + + /** + * Initializes the entity, loading all collections and proxies. + * @param entity The entity to initialize. + */ + public void initialize(HardwareScopeEntity entity) { + if (entity == null) { + return; + } + + Hibernate.initialize(entity); + Hibernate.initialize(entity.getIds()); + } + + /** + * Initializes the entity, loading all collections and proxies. + * @param entity The entity to initialize. + */ + public void initialize(final HardwareEntity entity) { + if (entity == null) { + return; + } + + Hibernate.initialize(entity); + + Hibernate.initialize(entity.getConstraints()); + if (entity.getConstraints() != null) { + for (final HardwareConstraintEntity constraint : entity.getConstraints()) { + Hibernate.initialize(constraint.getConstraintValues()); + } + } + + Hibernate.initialize(entity.getEnvironmentVariables()); + Hibernate.initialize(entity.getGenericResources()); + } +} diff --git a/src/main/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApi.java b/src/main/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApi.java new file mode 100644 index 0000000..cdba6a3 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApi.java @@ -0,0 +1,129 @@ +package org.nrg.jobtemplates.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import org.nrg.framework.annotations.XapiRestController; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.jobtemplates.models.ComputeSpecConfig; +import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.xapi.rest.AbstractXapiRestController; +import org.nrg.xapi.rest.XapiRequestMapping; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static org.nrg.xdat.security.helpers.AccessLevel.Admin; +import static org.nrg.xdat.security.helpers.AccessLevel.Read; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@Api("ComputeSpecConfigs REST API") +@XapiRestController +@RequestMapping(value = "/job-templates/compute-spec-configs") +public class ComputeSpecConfigsApi extends AbstractXapiRestController { + + private final ComputeSpecConfigService computeSpecConfigService; + + @Autowired + public ComputeSpecConfigsApi(final UserManagementServiceI userManagementService, + final RoleHolder roleHolder, + final ComputeSpecConfigService computeSpecConfigService) { + super(userManagementService, roleHolder); + this.computeSpecConfigService = computeSpecConfigService; + } + + @ApiOperation(value = "Get all compute spec configs or all compute spec configs for a given type.", response = ComputeSpecConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Compute spec configs successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Admin) + public List getAll(@RequestParam(value = "type", required = false) final ComputeSpecConfig.ConfigType type) { + if (type != null) { + return computeSpecConfigService.getByType(type); + } else { + return computeSpecConfigService.getAll(); + } + } + + @ApiOperation(value = "Get a compute spec config.", response = ComputeSpecConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Compute spec config successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Compute spec config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Admin) + public ComputeSpecConfig get(@PathVariable("id") final Long id) throws NotFoundException { + return computeSpecConfigService.retrieve(id) + .orElseThrow(() -> new NotFoundException("Compute spec config not found.")); + } + + @ApiOperation(value = "Create a compute spec config.", response = ComputeSpecConfig.class) + @ApiResponses({ + @ApiResponse(code = 201, message = "Compute spec config successfully created."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.CREATED) + @XapiRequestMapping(value = "",consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.POST, restrictTo = Admin) + public ComputeSpecConfig create(@RequestBody final ComputeSpecConfig computeSpecConfig) { + return computeSpecConfigService.create(computeSpecConfig); + } + + @ApiOperation(value = "Update a compute spec config.", response = ComputeSpecConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Compute spec config successfully updated."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Compute spec config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}",consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.PUT, restrictTo = Admin) + public ComputeSpecConfig update(@PathVariable("id") final Long id, + @RequestBody final ComputeSpecConfig computeSpecConfig) throws NotFoundException { + if (!id.equals(computeSpecConfig.getId())) { + throw new IllegalArgumentException("The ID in the path must match the ID in the body."); + } + + return computeSpecConfigService.update(computeSpecConfig); + } + + @ApiOperation(value = "Delete a compute spec config.") + @ApiResponses({ + @ApiResponse(code = 204, message = "Compute spec config successfully deleted."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Compute spec config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + @XapiRequestMapping(value = "/{id}", method = RequestMethod.DELETE, restrictTo = Admin) + public void delete(@PathVariable("id") final Long id) throws NotFoundException { + computeSpecConfigService.delete(id); + } + + @ApiOperation(value = "Get all available compute spec configs for the given user and project.", response = ComputeSpecConfig.class, responseContainer = "List") + @ApiResponses({ + @ApiResponse(code = 200, message = "Compute spec configs successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/available", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Read) + public List getAvailable(@RequestParam(value = "user") final String user, + @RequestParam(value = "project") final String project, + @RequestParam(value = "type", required = false) final ComputeSpecConfig.ConfigType type) { + return computeSpecConfigService.getAvailable(user, project, type); + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/rest/ConstraintConfigsApi.java b/src/main/java/org/nrg/jobtemplates/rest/ConstraintConfigsApi.java new file mode 100644 index 0000000..25edc4e --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/rest/ConstraintConfigsApi.java @@ -0,0 +1,115 @@ +package org.nrg.jobtemplates.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import org.nrg.framework.annotations.XapiRestController; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.jobtemplates.models.ConstraintConfig; +import org.nrg.jobtemplates.services.ConstraintConfigService; +import org.nrg.xapi.rest.AbstractXapiRestController; +import org.nrg.xapi.rest.XapiRequestMapping; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.util.List; + +import static org.nrg.xdat.security.helpers.AccessLevel.Admin; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.web.bind.annotation.RequestMethod.*; + +@Api("Constraint Configs REST API") +@XapiRestController +@RequestMapping(value = "/job-templates/constraint-configs") +public class ConstraintConfigsApi extends AbstractXapiRestController { + + private final ConstraintConfigService constraintConfigService; + + @Autowired + public ConstraintConfigsApi(final UserManagementServiceI userManagementService, + final RoleHolder roleHolder, + final ConstraintConfigService constraintConfigService) { + super(userManagementService, roleHolder); + this.constraintConfigService = constraintConfigService; + } + + @ApiOperation(value = "Get a constraint config.", response = ConstraintConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Placement constraint config successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Placement constraint config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Admin) + public ConstraintConfig get(@PathVariable final Long id) throws NotFoundException { + return constraintConfigService.retrieve(id) + .orElseThrow(() -> new NotFoundException("No placement constraint config found with ID " + id)); + } + + @ApiOperation(value = "Get all constraint configs.", response = ConstraintConfig.class, responseContainer = "List") + @ApiResponses({ + @ApiResponse(code = 200, message = "Placement constraint configs successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Admin) + public List getAll() { + return constraintConfigService.getAll(); + } + + @ApiOperation(value = "Create a constraint config.", response = ConstraintConfig.class) + @ApiResponses({ + @ApiResponse(code = 201, message = "Placement constraint config successfully created."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(CREATED) + @XapiRequestMapping(value = "", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = POST, restrictTo = Admin) + public ConstraintConfig create(@RequestBody final ConstraintConfig constraintConfig) { + return constraintConfigService.create(constraintConfig); + } + + @ApiOperation(value = "Update a constraint config.", response = ConstraintConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Placement constraint config successfully updated."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Placement constraint config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = PUT, restrictTo = Admin) + public ConstraintConfig update(@PathVariable final Long id, + @RequestBody final ConstraintConfig constraintConfig) throws NotFoundException { + if (!id.equals(constraintConfig.getId())) { + throw new IllegalArgumentException("Placement constraint config ID in path does not match ID in body."); + } + + return constraintConfigService.update(constraintConfig); + } + + @ApiOperation(value = "Delete a constraint config.") + @ApiResponses({ + @ApiResponse(code = 204, message = "Placement constraint config successfully deleted."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Placement constraint config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + @XapiRequestMapping(value = "/{id}", method = DELETE, restrictTo = Admin) + public void delete(@PathVariable final Long id) throws NotFoundException { + constraintConfigService.delete(id); + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/rest/HardwareConfigsApi.java b/src/main/java/org/nrg/jobtemplates/rest/HardwareConfigsApi.java new file mode 100644 index 0000000..c76bf88 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/rest/HardwareConfigsApi.java @@ -0,0 +1,112 @@ +package org.nrg.jobtemplates.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import org.nrg.framework.annotations.XapiRestController; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.jobtemplates.models.HardwareConfig; +import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xapi.rest.AbstractXapiRestController; +import org.nrg.xapi.rest.XapiRequestMapping; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.util.List; + +import static org.nrg.xdat.security.helpers.AccessLevel.Admin; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.web.bind.annotation.RequestMethod.*; + +@Api("Hardware Configs REST API") +@XapiRestController +@RequestMapping(value = "/job-templates/hardware-configs") +public class HardwareConfigsApi extends AbstractXapiRestController { + + private final HardwareConfigService hardwareConfigService; + + @Autowired + public HardwareConfigsApi(final UserManagementServiceI userManagementService, + final RoleHolder roleHolder, + final HardwareConfigService hardwareConfigService) { + super(userManagementService, roleHolder); + this.hardwareConfigService = hardwareConfigService; + } + + @ApiOperation(value = "Get a hardware config.", response = HardwareConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Hardware config successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Hardware config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Admin) + public HardwareConfig get(@PathVariable("id") final Long id) throws NotFoundException { + return hardwareConfigService.retrieve(id).orElseThrow(() -> new NotFoundException("No hardware config found for ID " + id)); + } + + @ApiOperation(value = "Get all hardware configs.", response = HardwareConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Hardware configs successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Admin) + public List getAll() { + return hardwareConfigService.retrieveAll(); + } + + @ApiOperation(value = "Create a hardware config.", response = HardwareConfig.class) + @ApiResponses({ + @ApiResponse(code = 201, message = "Hardware config successfully created."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.CREATED) + @XapiRequestMapping(value = "", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = POST, restrictTo = Admin) + public HardwareConfig create(@RequestBody final HardwareConfig hardwareConfig) { + return hardwareConfigService.create(hardwareConfig); + } + + @ApiOperation(value = "Update a hardware config.", response = HardwareConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Hardware config successfully updated."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Hardware config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}", consumes = APPLICATION_JSON_VALUE, method = PUT, restrictTo = Admin) + public HardwareConfig update(@PathVariable("id") final Long id, + @RequestBody final HardwareConfig hardwareConfig) throws NotFoundException { + if (!id.equals(hardwareConfig.getId())) { + throw new IllegalArgumentException("The hardware config ID in the path must match the ID in the body."); + } + return hardwareConfigService.update(hardwareConfig); + } + + @ApiOperation(value = "Delete a hardware config.") + @ApiResponses({ + @ApiResponse(code = 204, message = "Hardware config successfully deleted."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Hardware config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + @XapiRequestMapping(value = "/{id}", method = DELETE, restrictTo = Admin) + public void delete(@PathVariable("id") final Long id) throws NotFoundException { + hardwareConfigService.delete(id); + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigEntityService.java b/src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigEntityService.java new file mode 100644 index 0000000..1ffeb07 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigEntityService.java @@ -0,0 +1,15 @@ +package org.nrg.jobtemplates.services; + +import org.nrg.framework.orm.hibernate.BaseHibernateService; +import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; +import org.nrg.jobtemplates.models.ComputeSpecConfig; + +import java.util.List; + +public interface ComputeSpecConfigEntityService extends BaseHibernateService { + + void addHardwareConfigEntity(Long computeSpecConfigId, Long hardwareConfigId); + void removeHardwareConfigEntity(Long computeSpecConfigId, Long hardwareConfigId); + List findByType(ComputeSpecConfig.ConfigType type); + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigService.java b/src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigService.java new file mode 100644 index 0000000..1525841 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigService.java @@ -0,0 +1,22 @@ +package org.nrg.jobtemplates.services; + +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.jobtemplates.models.ComputeSpecConfig; + +import java.util.List; +import java.util.Optional; + +public interface ComputeSpecConfigService { + + boolean exists(Long id); + Optional retrieve(Long id); + List getAll(); + List getByType(ComputeSpecConfig.ConfigType type); + List getAvailable(String user, String project); + List getAvailable(String user, String project, ComputeSpecConfig.ConfigType type); + boolean isAvailable(String user, String project, Long id); + ComputeSpecConfig create(ComputeSpecConfig computeSpecConfig); + ComputeSpecConfig update(ComputeSpecConfig computeSpecConfig) throws NotFoundException; + void delete(Long id) throws NotFoundException; + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/ConstraintConfigEntityService.java b/src/main/java/org/nrg/jobtemplates/services/ConstraintConfigEntityService.java new file mode 100644 index 0000000..a1db025 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/ConstraintConfigEntityService.java @@ -0,0 +1,8 @@ +package org.nrg.jobtemplates.services; + +import org.nrg.framework.orm.hibernate.BaseHibernateService; +import org.nrg.jobtemplates.entities.ConstraintConfigEntity; + +public interface ConstraintConfigEntityService extends BaseHibernateService { + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/ConstraintConfigService.java b/src/main/java/org/nrg/jobtemplates/services/ConstraintConfigService.java new file mode 100644 index 0000000..43a02c4 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/ConstraintConfigService.java @@ -0,0 +1,18 @@ +package org.nrg.jobtemplates.services; + +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.jobtemplates.models.ConstraintConfig; + +import java.util.List; +import java.util.Optional; + +public interface ConstraintConfigService { + + Optional retrieve(Long id); + List getAll(); + List getAvailable(String project); + ConstraintConfig create(ConstraintConfig config); + ConstraintConfig update(ConstraintConfig config) throws NotFoundException; + void delete(Long id); + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/HardwareConfigEntityService.java b/src/main/java/org/nrg/jobtemplates/services/HardwareConfigEntityService.java new file mode 100644 index 0000000..47fcb74 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/HardwareConfigEntityService.java @@ -0,0 +1,8 @@ +package org.nrg.jobtemplates.services; + +import org.nrg.framework.orm.hibernate.BaseHibernateService; +import org.nrg.jobtemplates.entities.HardwareConfigEntity; + +public interface HardwareConfigEntityService extends BaseHibernateService { + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/HardwareConfigService.java b/src/main/java/org/nrg/jobtemplates/services/HardwareConfigService.java new file mode 100644 index 0000000..aba3249 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/HardwareConfigService.java @@ -0,0 +1,19 @@ +package org.nrg.jobtemplates.services; + +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.jobtemplates.models.HardwareConfig; + +import java.util.List; +import java.util.Optional; + +public interface HardwareConfigService { + + boolean exists(Long id); + Optional retrieve(Long id); + List retrieveAll(); + HardwareConfig create(HardwareConfig hardwareConfig); + HardwareConfig update(HardwareConfig hardwareConfig) throws NotFoundException; + void delete(Long id) throws NotFoundException; + boolean isAvailable(String user, String project, Long id); + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/JobTemplateService.java b/src/main/java/org/nrg/jobtemplates/services/JobTemplateService.java new file mode 100644 index 0000000..7fc9023 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/JobTemplateService.java @@ -0,0 +1,10 @@ +package org.nrg.jobtemplates.services; + +import org.nrg.jobtemplates.models.JobTemplate; + +public interface JobTemplateService { + + boolean isAvailable(String user, String project, Long computeSpecConfigId, Long hardwareConfigId); + JobTemplate resolve(String user, String project, Long computeSpecConfigId, Long hardwareConfigId); + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigService.java b/src/main/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigService.java new file mode 100644 index 0000000..92c9fb5 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigService.java @@ -0,0 +1,397 @@ +package org.nrg.jobtemplates.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.nrg.framework.constants.Scope; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; +import org.nrg.jobtemplates.entities.HardwareConfigEntity; +import org.nrg.jobtemplates.models.*; +import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; +import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.jobtemplates.services.HardwareConfigEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class DefaultComputeSpecConfigService implements ComputeSpecConfigService { + + private final ComputeSpecConfigEntityService computeSpecConfigEntityService; + private final HardwareConfigEntityService hardwareConfigEntityService; + + @Autowired + public DefaultComputeSpecConfigService(final ComputeSpecConfigEntityService computeSpecConfigEntityService, + final HardwareConfigEntityService hardwareConfigEntityService) { + this.computeSpecConfigEntityService = computeSpecConfigEntityService; + this.hardwareConfigEntityService = hardwareConfigEntityService; + } + + /** + * Checks if a ComputeSpecConfig with the given ID exists. + * @param id The ID of the ComputeSpecConfig to check for. + * @return True if a ComputeSpecConfig with the given ID exists, false otherwise. + */ + @Override + public boolean exists(Long id) { + return computeSpecConfigEntityService.exists("id", id); + } + + /** + * Gets a ComputeSpecConfig by its ID. + * @param id The ID of the ComputeSpecConfig to retrieve. + * @return The ComputeSpecConfig with the given ID, if it exists or else an empty Optional. + */ + @Override + public Optional retrieve(Long id) { + ComputeSpecConfigEntity entity = computeSpecConfigEntityService.retrieve(id); + return Optional.ofNullable(entity) + .map(ComputeSpecConfigEntity::toPojo); + } + + /** + * Get all ComputeSpecConfigs. + * @return A list of all ComputeSpecConfigs. + */ + @Override + public List getAll() { + return computeSpecConfigEntityService + .getAll() + .stream() + .map(ComputeSpecConfigEntity::toPojo) + .collect(Collectors.toList()); + } + + /** + * Creates a new ComputeSpecConfig. + * @param type The type of the ComputeSpecConfig to create. + * @return The newly created ComputeSpecConfig. + */ + @Override + public List getByType(ComputeSpecConfig.ConfigType type) { + return computeSpecConfigEntityService + .findByType(type) + .stream() + .map(ComputeSpecConfigEntity::toPojo) + .collect(Collectors.toList()); + } + + /** + * Get all ComputeSpecConfigs that are available to the given user and project regardless of type. + * @param user The user to check for. + * @param project The project to check for. + * @return A list of all ComputeSpecConfigs that are available to the given user and project. + */ + @Override + public List getAvailable(String user, String project) { + return getAvailable(user, project, null); + } + + /** + * Get all ComputeSpecConfigs of the given type that are available to the given user and project. + * @param user The user to check for. + * @param project The project to check for. + * @param type The type of ComputeSpecConfig to check for. + * @return A list of all ComputeSpecConfigs of the given type that are available to the given user and project. + */ + @Override + public List getAvailable(String user, String project, ComputeSpecConfig.ConfigType type) { + List all; + + if (type == null) { + all = getAll(); + } else { + all = getByType(type); + } + + final List available = all.stream().filter(computeSpecConfig -> { + final ComputeSpecScope siteScope = computeSpecConfig.getScopes().get(Scope.Site); + final ComputeSpecScope projectScope = computeSpecConfig.getScopes().get(Scope.Project); + final ComputeSpecScope userScope = computeSpecConfig.getScopes().get(Scope.User); + + return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); + }).collect(Collectors.toList()); + + available.forEach(computeSpecConfig -> { + final Set hardwareConfigs = computeSpecConfig.getHardwareOptions().getHardwareConfigs(); + final Set availableHardware = hardwareConfigs.stream() + .filter(hardwareConfig -> { + final HardwareScope siteScope = hardwareConfig.getScopes().get(Scope.Site); + final HardwareScope projectScope = hardwareConfig.getScopes().get(Scope.Project); + final HardwareScope userScope = hardwareConfig.getScopes().get(Scope.User); + + return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); + }).collect(Collectors.toSet()); + + computeSpecConfig.getHardwareOptions().setHardwareConfigs(availableHardware); + }); + + return available; + } + + /** + * Checks if the given ComputeSpecConfig is available to the given user and project. + * @param user The user to check for. + * @param project The project to check for. + * @param id The ID of the ComputeSpecConfig to check for. + * @return True if the ComputeSpecConfig with the given ID is available to the given user and project, false otherwise. + */ + @Override + public boolean isAvailable(String user, String project, Long id) { + final Optional computeSpecConfig = retrieve(id); + + if (!computeSpecConfig.isPresent()) { + return false; + } + + final ComputeSpecScope siteScope = computeSpecConfig.get().getScopes().get(Scope.Site); + final ComputeSpecScope projectScope = computeSpecConfig.get().getScopes().get(Scope.Project); + final ComputeSpecScope userScope = computeSpecConfig.get().getScopes().get(Scope.User); + + return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); + } + + /** + * Creates a new ComputeSpecConfig. + * @param computeSpecConfig The ComputeSpecConfig to create. + * @return The newly created ComputeSpecConfig, with its ID set. + */ + @Override + public ComputeSpecConfig create(ComputeSpecConfig computeSpecConfig) { + // Validate the ComputeSpecConfig + validate(computeSpecConfig); + + // Create the new compute spec config entity + ComputeSpecConfigEntity newComputeSpecConfigEntity = computeSpecConfigEntityService.create(ComputeSpecConfigEntity.fromPojo(computeSpecConfig)); + + // Update the pojo with the new ID, needed for association with hardware configs + computeSpecConfig.setId(newComputeSpecConfigEntity.getId()); + + setHardwareConfigsForComputeSpecConfig(computeSpecConfig); + + return computeSpecConfigEntityService.retrieve(newComputeSpecConfigEntity.getId()).toPojo(); + } + + /** + * Updates an existing ComputeSpecConfig. + * @param computeSpecConfig The ComputeSpecConfig to update. + * @return The updated ComputeSpecConfig. + * @throws NotFoundException If the ComputeSpecConfig to update does not exist. + */ + @Override + public ComputeSpecConfig update(ComputeSpecConfig computeSpecConfig) throws NotFoundException { + // Validate the ComputeSpecConfig + if (computeSpecConfig.getId() == null) { + throw new IllegalArgumentException("ComputeSpecConfig ID cannot be null when updating"); + } + + validate(computeSpecConfig); + + ComputeSpecConfigEntity template = computeSpecConfigEntityService.get(computeSpecConfig.getId()); + template.update(computeSpecConfig); + computeSpecConfigEntityService.update(template); + + // Update the pojo with the new ID, needed for association with hardware configs + computeSpecConfig.setId(template.getId()); + + setHardwareConfigsForComputeSpecConfig(computeSpecConfig); + + return computeSpecConfigEntityService.get(computeSpecConfig.getId()).toPojo(); + } + + /** + * Deletes an existing ComputeSpecConfig. + * @param id The ID of the ComputeSpecConfig to delete. + * @throws NotFoundException If the ComputeSpecConfig to delete does not exist. + */ + @Override + public void delete(Long id) throws NotFoundException { + if (!exists(id)) { + throw new NotFoundException("No compute spec config found with ID " + id); + } + + // Remove all hardware configs from the compute spec config + // Get the ids of all hardware configs associated with the compute spec config before deleting to avoid + // a ConcurrentModificationException + Set hardwareConfigIds = computeSpecConfigEntityService.retrieve(id) + .getHardwareOptions() + .getHardwareConfigs() + .stream() + .map(HardwareConfigEntity::getId) + .collect(Collectors.toSet()); + + hardwareConfigIds.forEach(hardwareConfigId -> { + computeSpecConfigEntityService.removeHardwareConfigEntity(id, hardwareConfigId); + }); + + computeSpecConfigEntityService.delete(id); + } + + /** + * Checks if the scope is enabled for the given user and project. + * @param user The user to check for. + * @param project The project to check for. + * @param siteScope The site scope to check. + * @param userScope The user scope to check. + * @param projectScope The project scope to check. + * @return True if the scopes are enabled for the given user and project, false otherwise. + */ + protected boolean isScopeEnabledForSiteUserAndProject(String user, String project, ComputeSpecScope siteScope, ComputeSpecScope userScope, ComputeSpecScope projectScope) { + return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled + userScope != null && (userScope.isEnabled() || userScope.getIds().contains(user)) && // User scope must be enabled for all users or the user must be in the list + projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list + } + + /** + * Checks if the scope is enabled for the given user and project. + * @param user The user to check for. + * @param project The project to check for. + * @param siteScope The site scope to check. + * @param userScope The user scope to check. + * @param projectScope The project scope to check. + * @return True if the scopes are enabled for the given user and project, false otherwise. + */ + protected boolean isScopeEnabledForSiteUserAndProject(String user, String project, HardwareScope siteScope, HardwareScope userScope, HardwareScope projectScope) { + return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled + userScope != null && (userScope.isEnabled() || userScope.getIds().contains(user)) && // User scope must be enabled for all users or the user must be in the list + projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list + } + + /** + * Connect the hardware config entities to the compute spec config entity. + * @param computeSpecConfig The compute spec config to connect the hardware configs to. + */ + protected void setHardwareConfigsForComputeSpecConfig(ComputeSpecConfig computeSpecConfig) { + // Probably a more efficient way to do this, but it works for now + // Remove all hardware configs from the compute spec config + // Get the ids of all hardware configs associated with the compute spec config before deleting to avoid + // a ConcurrentModificationException + Set hardwareConfigIds = computeSpecConfigEntityService.retrieve(computeSpecConfig.getId()) + .getHardwareOptions() + .getHardwareConfigs() + .stream() + .map(HardwareConfigEntity::getId) + .collect(Collectors.toSet()); + + hardwareConfigIds.forEach(hardwareConfigId -> { + computeSpecConfigEntityService.removeHardwareConfigEntity(computeSpecConfig.getId(), hardwareConfigId); + }); + + // Add the hardware configs to the compute spec config + if (computeSpecConfig.getHardwareOptions().isAllowAllHardware()) { + // Add all hardware configs to the compute spec config + hardwareConfigEntityService.getAll().forEach(hardwareConfigEntity -> { + computeSpecConfigEntityService.addHardwareConfigEntity(computeSpecConfig.getId(), hardwareConfigEntity.getId()); + }); + } else { + // Add the specified hardware configs to the compute spec config + computeSpecConfig.getHardwareOptions().getHardwareConfigs().forEach(hardwareConfig -> { + if (hardwareConfig.getId() == null) { + // cant add a hardware config that doesn't exist + return; + } + + HardwareConfigEntity hardwareConfigEntity = hardwareConfigEntityService.retrieve(hardwareConfig.getId()); + + if (hardwareConfigEntity == null) { + // cant add a hardware config that doesn't exist + return; + } + + computeSpecConfigEntityService.addHardwareConfigEntity(computeSpecConfig.getId(), hardwareConfigEntity.getId()); + }); + } + } + + /** + * Validates the given ComputeSpecConfig. Throws an IllegalArgumentException if the ComputeSpecConfig is invalid. + * @param config The ComputeSpecConfig to validate. + */ + protected void validate(ComputeSpecConfig config) { + if (config == null) { + throw new IllegalArgumentException("ComputeSpecConfig cannot be null"); + } + + List errors = new ArrayList<>(); + + errors.addAll(validate(config.getConfigTypes())); + errors.addAll(validate(config.getComputeSpec())); + errors.addAll(validate(config.getScopes())); + errors.addAll(validate(config.getHardwareOptions())); + + if (!errors.isEmpty()) { + throw new IllegalArgumentException("ComputeSpecConfig is invalid: " + errors); + } + } + + private List validate(final Set configTypes) { + List errors = new ArrayList<>(); + + if (configTypes == null || configTypes.isEmpty()) { + errors.add("ComputeSpecConfig must have at least one config type"); + } + + return errors; + } + + private List validate(final ComputeSpec computeSpec) { + List errors = new ArrayList<>(); + + if (computeSpec == null) { + errors.add("ComputeSpec cannot be null"); + return errors; + } + + if (StringUtils.isBlank(computeSpec.getName())) { + errors.add("ComputeSpec name cannot be blank"); + } + + if (StringUtils.isBlank(computeSpec.getImage())) { + errors.add("ComputeSpec image cannot be blank"); + } + + return errors; + } + + private List validate(final Map scopes) { + List errors = new ArrayList<>(); + + if (scopes == null || scopes.isEmpty()) { + errors.add("ComputeSpecConfig must have at least one scope"); + return errors; + } + + if (!scopes.containsKey(Scope.Site)) { + errors.add("ComputeSpecConfig must have a site scope"); + } + + if (!scopes.containsKey(Scope.User)) { + errors.add("ComputeSpecConfig must have a user scope"); + } + + if (!scopes.containsKey(Scope.Project)) { + errors.add("ComputeSpecConfig must have a project scope"); + } + + return errors; + } + + private List validate(final ComputeSpecHardwareOptions hardwareOptions) { + List errors = new ArrayList<>(); + + if (hardwareOptions == null) { + errors.add("ComputeSpecHardwareOptions cannot be null"); + return errors; + } + + if (hardwareOptions.getHardwareConfigs() == null) { + errors.add("ComputeSpecHardwareOptions hardware configs cannot be null"); + } + + return errors; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigService.java b/src/main/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigService.java new file mode 100644 index 0000000..dae670a --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigService.java @@ -0,0 +1,193 @@ +package org.nrg.jobtemplates.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.nrg.framework.constants.Scope; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.jobtemplates.entities.ConstraintConfigEntity; +import org.nrg.jobtemplates.models.Constraint; +import org.nrg.jobtemplates.models.ConstraintConfig; +import org.nrg.jobtemplates.models.ConstraintScope; +import org.nrg.jobtemplates.services.ConstraintConfigEntityService; +import org.nrg.jobtemplates.services.ConstraintConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class DefaultConstraintConfigService implements ConstraintConfigService { + + private final ConstraintConfigEntityService constraintConfigEntityService; + + @Autowired + public DefaultConstraintConfigService(ConstraintConfigEntityService constraintConfigEntityService) { + this.constraintConfigEntityService = constraintConfigEntityService; + } + + /** + * Returns the constraint config with the given id. + * @param id The id of the constraint config to retrieve + * @return The constraint config with the given id + */ + @Override + public Optional retrieve(Long id) { + ConstraintConfigEntity entity = constraintConfigEntityService.retrieve(id); + return Optional.ofNullable(entity) + .map(ConstraintConfigEntity::toPojo); + } + + /** + * Get all constraint configs. + * @return List of all constraint configs + */ + @Override + public List getAll() { + return constraintConfigEntityService + .getAll() + .stream() + .map(ConstraintConfigEntity::toPojo) + .collect(Collectors.toList()); + } + + /** + * Returns all constraint configs that are available for the given project. + * @param project The project to get constraint configs for + * @return All constraint configs that are available for the given project + */ + @Override + public List getAvailable(String project) { + return constraintConfigEntityService + .getAll() + .stream() + .map(ConstraintConfigEntity::toPojo) + .filter(config -> isScopeEnabledForSiteAndProject(project, config.getScopes().get(Scope.Site), config.getScopes().get(Scope.Project))) + .collect(Collectors.toList()); + } + + /** + * Creates a new constraint config. + * @param config The constraint config to create + * @return The created constraint config + */ + @Override + public ConstraintConfig create(ConstraintConfig config) { + // Validate + validate(config); + + // Create + ConstraintConfigEntity entity = constraintConfigEntityService.create(ConstraintConfigEntity.fromPojo(config)); + return constraintConfigEntityService.retrieve(entity.getId()).toPojo(); + } + + /** + * Updates the given constraint config. + * @param config The constraint config to update + * @return The updated constraint config + * @throws NotFoundException If the constraint config does not exist + */ + @Override + public ConstraintConfig update(ConstraintConfig config) throws NotFoundException { + // Validate + if (config.getId() == null) { + throw new IllegalArgumentException("Constraint config id cannot be null when updating"); + } + + validate(config); + + // Update + ConstraintConfigEntity template = constraintConfigEntityService.retrieve(config.getId()); + template.update(config); + constraintConfigEntityService.update(template); + return constraintConfigEntityService.retrieve(template.getId()).toPojo(); + } + + /** + * Deletes the constraint config with the given id. + * @param id The id of the constraint config to delete + */ + @Override + public void delete(Long id) { + constraintConfigEntityService.delete(id); + } + + /** + * Returns true if the site scope is enabled and the project scope is enabled for all projects or the project is in + * the list of enabled projects. + * @param project The project to check + * @param siteScope The site scope + * @param projectScope The project scope + * @return True if the site scope is enabled and the project scope is enabled for all projects or the project is in + * the list of enabled projects + */ + protected boolean isScopeEnabledForSiteAndProject(String project, ConstraintScope siteScope, ConstraintScope projectScope) { + return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled + projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list + } + + /** + * Validates that the given constraint config is valid. Throws an IllegalArgumentException if it is not. + * @param config + */ + protected void validate(ConstraintConfig config) { + if (config == null) { + throw new IllegalArgumentException("Constraint config cannot be null"); + } + + List errors = new ArrayList<>(); + + errors.addAll(validate(config.getConstraint())); + errors.addAll(validate(config.getScopes())); + + if (!errors.isEmpty()) { + throw new IllegalArgumentException("Constraint config is invalid: " + String.join(", ", errors)); + } + } + + private List validate(Constraint constraint) { + List errors = new ArrayList<>(); + + if (constraint == null) { + errors.add("Constraint cannot be null"); + return errors; + } + + if (StringUtils.isBlank(constraint.getKey())) { + errors.add("Constraint key cannot be null or blank"); + } + + if (constraint.getValues() == null || constraint.getValues().isEmpty()) { + errors.add("Constraint values cannot be null or empty"); + } + + if (constraint.getOperator() == null) { + errors.add("Constraint operator cannot be null"); + } + + return errors; + } + + private List validate(Map scopes) { + List errors = new ArrayList<>(); + + if (scopes == null || scopes.isEmpty()) { + errors.add("Scopes cannot be null or empty"); + return errors; + } + + if (!scopes.containsKey(Scope.Site)) { + errors.add("Scopes must contain a site scope"); + } + + if (!scopes.containsKey(Scope.Project)) { + errors.add("Scopes must contain a project scope"); + } + + return errors; + } +} diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigService.java b/src/main/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigService.java new file mode 100644 index 0000000..2bfa06b --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigService.java @@ -0,0 +1,227 @@ +package org.nrg.jobtemplates.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.nrg.framework.constants.Scope; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; +import org.nrg.jobtemplates.entities.ComputeSpecHardwareOptionsEntity; +import org.nrg.jobtemplates.entities.HardwareConfigEntity; +import org.nrg.jobtemplates.models.Hardware; +import org.nrg.jobtemplates.models.HardwareConfig; +import org.nrg.jobtemplates.models.HardwareScope; +import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; +import org.nrg.jobtemplates.services.HardwareConfigEntityService; +import org.nrg.jobtemplates.services.HardwareConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class DefaultHardwareConfigService implements HardwareConfigService { + + private final HardwareConfigEntityService hardwareConfigEntityService; + private final ComputeSpecConfigEntityService computeSpecConfigEntityService; + + @Autowired + public DefaultHardwareConfigService(final HardwareConfigEntityService hardwareConfigEntityService, + final ComputeSpecConfigEntityService computeSpecConfigEntityService) { + this.hardwareConfigEntityService = hardwareConfigEntityService; + this.computeSpecConfigEntityService = computeSpecConfigEntityService; + } + + /** + * Checks if a hardware config with the given id exists. + * @param id The id of the hardware config to check for + * @return True if a hardware config with the given id exists, false otherwise + */ + @Override + public boolean exists(Long id) { + return hardwareConfigEntityService.exists("id", id); + } + + /** + * Returns the hardware config with the given id. + * @param id The id of the hardware config to retrieve + * @return An optional containing the hardware config with the given id, or empty if no such hardware config exists + */ + @Override + public Optional retrieve(Long id) { + HardwareConfigEntity entity = hardwareConfigEntityService.retrieve(id); + return Optional.ofNullable(entity).map(HardwareConfigEntity::toPojo); + } + + /** + * Returns all hardware configs + * @return List of all hardware configs + */ + @Override + public List retrieveAll() { + return hardwareConfigEntityService + .getAll() + .stream() + .map(HardwareConfigEntity::toPojo) + .collect(Collectors.toList()); + } + + /** + * Creates a new hardware config. + * @param hardwareConfig The hardware config to create + * @return The newly created hardware config with an id + */ + @Override + public HardwareConfig create(HardwareConfig hardwareConfig) { + // Validate the hardware config + validate(hardwareConfig); + + // Create the new hardware config entity + HardwareConfigEntity hardwareConfigEntity = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig)); + + // Add the hardware config to all compute spec configs that allow all hardware + computeSpecConfigEntityService.getAll().stream() + .map(ComputeSpecConfigEntity::getHardwareOptions) + .filter(ComputeSpecHardwareOptionsEntity::isAllowAllHardware) + .forEach(hardwareOptions -> { + computeSpecConfigEntityService.addHardwareConfigEntity(hardwareOptions.getId(), hardwareConfigEntity.getId()); + }); + + return hardwareConfigEntityService.retrieve(hardwareConfigEntity.getId()).toPojo(); + } + + /** + * Updates an existing hardware config. + * @param hardwareConfig The hardware config to update + * @return The updated hardware config + * @throws NotFoundException If no hardware config exists with the given id + */ + @Override + public HardwareConfig update(HardwareConfig hardwareConfig) throws NotFoundException { + // Validate the hardware config + if (hardwareConfig.getId() == null) { + throw new IllegalArgumentException("Hardware config id cannot be null"); + } + + validate(hardwareConfig); + + // Update the hardware config + HardwareConfigEntity template = hardwareConfigEntityService.get(hardwareConfig.getId()); + template.update(hardwareConfig); + hardwareConfigEntityService.update(template); + return hardwareConfigEntityService.get(hardwareConfig.getId()).toPojo(); + } + + /** + * Deletes the hardware config with the given id. + * @param id The id of the hardware config to delete + * @throws NotFoundException If no hardware config exists with the given id + */ + @Override + public void delete(Long id) throws NotFoundException { + if (!exists(id)) { + throw new NotFoundException("No hardware config found with id " + id); + } + + // Remove the hardware config from all compute spec configs + // Probably a more efficient way to do this, but this is the easiest way to do it + computeSpecConfigEntityService.getAll().stream() + .map(ComputeSpecConfigEntity::getHardwareOptions) + .forEach(hardwareOptions -> { + computeSpecConfigEntityService.removeHardwareConfigEntity(hardwareOptions.getId(), id); + }); + + hardwareConfigEntityService.delete(id); + } + + /** + * Checks if the hardware config with the given id is available for the given user and project. + * @param user The user to check for + * @param project The project to check for + * @param id The id of the hardware config to check for + * @return True if the hardware config with the given id is available for the given user and project, false otherwise. + */ + @Override + public boolean isAvailable(String user, String project, Long id) { + final Optional hardwareConfig = retrieve(id); + + if (!hardwareConfig.isPresent()) { + log.error("No hardware config found with id " + id); + return false; + } + + final HardwareScope siteScope = hardwareConfig.get().getScopes().get(Scope.Site); + final HardwareScope projectScope = hardwareConfig.get().getScopes().get(Scope.Project); + final HardwareScope userScope = hardwareConfig.get().getScopes().get(Scope.User); + + return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); + } + + protected boolean isScopeEnabledForSiteUserAndProject(String user, String project, HardwareScope siteScope, HardwareScope userScope, HardwareScope projectScope) { + return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled + userScope != null && (userScope.isEnabled() || userScope.getIds().contains(user)) && // User scope must be enabled for all users or the user must be in the list + projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list + } + + /** + * Validates the given hardware config. Throws an IllegalArgumentException if the hardware config is invalid. + * @param hardwareConfig The hardware config to validate + */ + protected void validate(HardwareConfig hardwareConfig) { + if (hardwareConfig == null) { + throw new IllegalArgumentException("Hardware config cannot be null"); + } + + List errors = new ArrayList<>(); + + errors.addAll(validate(hardwareConfig.getHardware())); + errors.addAll(validate(hardwareConfig.getScopes())); + + if (!errors.isEmpty()) { + throw new IllegalArgumentException("Hardware config is invalid: " + String.join(", ", errors)); + } + } + + private List validate(final Hardware hardware) { + List errors = new ArrayList<>(); + + if (hardware == null) { + errors.add("Hardware cannot be null"); + return errors; + } + + if (StringUtils.isBlank(hardware.getName())) { + errors.add("Hardware name cannot be blank"); + } + + return errors; + } + + private List validate(final Map scopes) { + List errors = new ArrayList<>(); + + if (scopes == null || scopes.isEmpty()) { + errors.add("Hardware scopes cannot be null or empty"); + return errors; + } + + if (!scopes.containsKey(Scope.Site)) { + errors.add("Hardware scopes must contain a site scope"); + } + + if (!scopes.containsKey(Scope.User)) { + errors.add("Hardware scopes must contain a user scope"); + } + + if (!scopes.containsKey(Scope.Project)) { + errors.add("Hardware scopes must contain a project scope"); + } + + return errors; + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateService.java b/src/main/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateService.java new file mode 100644 index 0000000..a502508 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateService.java @@ -0,0 +1,107 @@ +package org.nrg.jobtemplates.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.jobtemplates.models.*; +import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.jobtemplates.services.ConstraintConfigService; +import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.jobtemplates.services.JobTemplateService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class DefaultJobTemplateService implements JobTemplateService { + + private final ComputeSpecConfigService computeSpecConfigService; + private final HardwareConfigService hardwareConfigService; + private final ConstraintConfigService constraintConfigService; + + @Autowired + public DefaultJobTemplateService(final ComputeSpecConfigService computeSpecConfigService, + final HardwareConfigService hardwareConfigService, + final ConstraintConfigService constraintConfigService) { + this.computeSpecConfigService = computeSpecConfigService; + this.hardwareConfigService = hardwareConfigService; + this.constraintConfigService = constraintConfigService; + } + + /** + * Returns true if the specified compute spec config and hardware config are available for the specified user and + * project and the hardware config is allowed by the compute spec config. + * @param user the user + * @param project the project + * @param computeSpecConfigId the compute spec config id + * @param hardwareConfigId the hardware config id + * @return true if the specified compute spec config and hardware config are available for the specified user and + * project and the hardware config is allowed by the compute spec config + */ + @Override + public boolean isAvailable(String user, String project, Long computeSpecConfigId, Long hardwareConfigId) { + if (user == null || project == null || computeSpecConfigId == null || hardwareConfigId == null) { + throw new IllegalArgumentException("One or more parameters is null"); + } + + boolean isComputeSpecConfigAvailable = computeSpecConfigService.isAvailable(user, project, computeSpecConfigId); + boolean isHardwareConfigAvailable = hardwareConfigService.isAvailable(user, project, hardwareConfigId); + + if (!isComputeSpecConfigAvailable || !isHardwareConfigAvailable) { + return false; + } + + ComputeSpecConfig computeSpecConfig = computeSpecConfigService + .retrieve(computeSpecConfigId) + .orElseThrow(() -> new IllegalArgumentException("ComputeSpecConfig with id " + computeSpecConfigId + " does not exist")); + + if (computeSpecConfig.getHardwareOptions().isAllowAllHardware()) { + return true; + } + + return computeSpecConfig.getHardwareOptions() + .getHardwareConfigs().stream() + .map(HardwareConfig::getId) + .anyMatch(id -> id.equals(hardwareConfigId)); + } + + /** + * Returns a job template for the specified user, project, compute spec config, and hardware config. + * @param user the user + * @param project the project + * @param computeSpecConfigId the compute spec config id + * @param hardwareConfigId the hardware config id + * @return a job template complete with compute spec and hardware + */ + @Override + public JobTemplate resolve(String user, String project, Long computeSpecConfigId, Long hardwareConfigId) { + if (user == null || project == null || computeSpecConfigId == null || hardwareConfigId == null) { + throw new IllegalArgumentException("One or more parameters is null"); + } + + if (!isAvailable(user, project, computeSpecConfigId, hardwareConfigId)) { + throw new IllegalArgumentException("JobTemplate with user " + user + ", project " + project + ", computeSpecConfigId " + computeSpecConfigId + ", and hardwareConfigId " + hardwareConfigId + " is not available"); + } + + ComputeSpecConfig computeSpecConfig = computeSpecConfigService + .retrieve(computeSpecConfigId) + .orElseThrow(() -> new IllegalArgumentException("ComputeSpecConfig with id " + computeSpecConfigId + " does not exist")); + + HardwareConfig hardwareConfig = hardwareConfigService + .retrieve(hardwareConfigId) + .orElseThrow(() -> new IllegalArgumentException("HardwareConfig with id " + hardwareConfigId + " does not exist")); + + List constraints = constraintConfigService.getAvailable(project).stream() + .map(ConstraintConfig::getConstraint) + .collect(Collectors.toList()); + + JobTemplate jobTemplate = JobTemplate.builder() + .computeSpec(computeSpecConfig.getComputeSpec()) + .hardware(hardwareConfig.getHardware()) + .constraints(constraints) + .build(); + + return jobTemplate; + } +} diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityService.java b/src/main/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityService.java new file mode 100644 index 0000000..29e5b50 --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityService.java @@ -0,0 +1,77 @@ +package org.nrg.jobtemplates.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; +import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; +import org.nrg.jobtemplates.entities.HardwareConfigEntity; +import org.nrg.jobtemplates.models.ComputeSpecConfig; +import org.nrg.jobtemplates.repositories.ComputeSpecConfigDao; +import org.nrg.jobtemplates.repositories.HardwareConfigDao; +import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; + +@Service +@Slf4j +public class HibernateComputeSpecConfigEntityService extends AbstractHibernateEntityService implements ComputeSpecConfigEntityService { + + private final HardwareConfigDao hardwareConfigDao; + + // For testing + public HibernateComputeSpecConfigEntityService(final ComputeSpecConfigDao computeSpecConfigDao, + final HardwareConfigDao hardwareConfigDao) { + super(); + this.hardwareConfigDao = hardwareConfigDao; + setDao(computeSpecConfigDao); + } + + @Autowired + public HibernateComputeSpecConfigEntityService(HardwareConfigDao hardwareConfigDao) { + this.hardwareConfigDao = hardwareConfigDao; + } + + /** + * Associates a hardware config with a compute spec config + * @param computeSpecConfigId The compute spec config id + * @param hardwareConfigId The hardware config id to associate with the compute spec config + */ + @Override + @Transactional + public void addHardwareConfigEntity(Long computeSpecConfigId, Long hardwareConfigId) { + ComputeSpecConfigEntity computeSpecConfigEntity = getDao().retrieve(computeSpecConfigId); + HardwareConfigEntity hardwareConfigEntity = hardwareConfigDao.retrieve(hardwareConfigId); + computeSpecConfigEntity.getHardwareOptions().addHardwareConfig(hardwareConfigEntity); + getDao().update(computeSpecConfigEntity); + hardwareConfigDao.update(hardwareConfigEntity); + } + + /** + * Removes association between a hardware config and a compute spec config + * @param computeSpecConfigId The compute spec config id + * @param hardwareConfigId The hardware config id to remove from the compute spec config + */ + @Override + @Transactional + public void removeHardwareConfigEntity(Long computeSpecConfigId, Long hardwareConfigId) { + ComputeSpecConfigEntity computeSpecConfigEntity = getDao().retrieve(computeSpecConfigId); + HardwareConfigEntity hardwareConfigEntity = hardwareConfigDao.retrieve(hardwareConfigId); + computeSpecConfigEntity.getHardwareOptions().removeHardwareConfig(hardwareConfigEntity); + getDao().update(computeSpecConfigEntity); + hardwareConfigDao.update(hardwareConfigEntity); + } + + /** + * Returns a list of compute spec configs by type + * @param type The type of compute spec configs to return + * @return A list of compute spec configs of the specified type + */ + @Override + @Transactional + public List findByType(ComputeSpecConfig.ConfigType type) { + return getDao().findByType(type.name()); + } + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityService.java b/src/main/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityService.java new file mode 100644 index 0000000..bfde7fd --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityService.java @@ -0,0 +1,14 @@ +package org.nrg.jobtemplates.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; +import org.nrg.jobtemplates.entities.ConstraintConfigEntity; +import org.nrg.jobtemplates.repositories.ConstraintConfigDao; +import org.nrg.jobtemplates.services.ConstraintConfigEntityService; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class HibernateConstraintConfigEntityService extends AbstractHibernateEntityService implements ConstraintConfigEntityService { + +} diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityService.java b/src/main/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityService.java new file mode 100644 index 0000000..0b6db7a --- /dev/null +++ b/src/main/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityService.java @@ -0,0 +1,14 @@ +package org.nrg.jobtemplates.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; +import org.nrg.jobtemplates.entities.HardwareConfigEntity; +import org.nrg.jobtemplates.repositories.HardwareConfigDao; +import org.nrg.jobtemplates.services.HardwareConfigEntityService; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class HibernateHardwareConfigEntityService extends AbstractHibernateEntityService implements HardwareConfigEntityService { + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java index 3333bd9..dbf2c84 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java @@ -2,6 +2,7 @@ import lombok.extern.slf4j.Slf4j; import org.nrg.framework.annotations.XnatPlugin; +import org.nrg.jobtemplates.config.JobTemplatesConfig; import org.nrg.xdat.security.helpers.UserHelper; import org.nrg.xdat.security.services.SearchHelperServiceI; import org.nrg.xnatx.plugins.jupyterhub.client.DefaultJupyterHubClient; @@ -10,10 +11,7 @@ import org.nrg.xnatx.plugins.jupyterhub.services.JupyterHubService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Scope; -import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.context.annotation.*; import org.springframework.scheduling.config.TriggerTask; import org.springframework.scheduling.support.PeriodicTrigger; @@ -22,7 +20,7 @@ @XnatPlugin(value = "JupyterHubPlugin", name = "Jupyter Hub Plugin", logConfigurationFile = "jupyterhub-logback.xml", - entityPackages = {"org.nrg.xnatx.plugins.jupyterhub.entities"}) + entityPackages = {"org.nrg.xnatx.plugins.jupyterhub.entities", "org.nrg.jobtemplates.entities"}) @ComponentScan({"org.nrg.xnatx.plugins.jupyterhub.preferences", "org.nrg.xnatx.plugins.jupyterhub.client", "org.nrg.xnatx.plugins.jupyterhub.rest", @@ -34,6 +32,7 @@ "org.nrg.xnatx.plugins.jupyterhub.utils", "org.nrg.xnatx.plugins.jupyterhub.authorization", "org.nrg.xnatx.plugins.jupyterhub.initialize"}) +@Import({JobTemplatesConfig.class}) @Slf4j public class JupyterHubPlugin { diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/ProfileEntity.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/ProfileEntity.java deleted file mode 100644 index bcf92c7..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/ProfileEntity.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.entities; - -import lombok.*; -import lombok.extern.slf4j.Slf4j; -import org.hibernate.annotations.Type; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.xnatx.plugins.jupyterhub.models.Profile; - -import javax.persistence.Basic; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; - -@Entity -@ToString -@Builder -@AllArgsConstructor -@NoArgsConstructor -@EqualsAndHashCode(callSuper = false) -@Slf4j -public class ProfileEntity extends AbstractHibernateEntity { - - private Profile profile; - - @Type(type = "jsonb") - @Column(columnDefinition = "jsonb", nullable = false) - @Basic(fetch=FetchType.EAGER) - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Profile toPojo() { - this.profile.setId(this.getId()); - return this.profile; - } - - public static ProfileEntity fromPojo(final Profile pojo) { - ProfileEntity profileEntity = ProfileEntity.builder() - .profile(pojo) - .build(); - - if (pojo.getId() != null) { - profileEntity.setId(pojo.getId()); - } - - return profileEntity; - } - - public ProfileEntity update(final ProfileEntity update) { - update.getProfile().setId(this.getId()); - this.setProfile(update.getProfile()); - return this; - } -} \ No newline at end of file diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java new file mode 100644 index 0000000..ca9082d --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java @@ -0,0 +1,249 @@ +package org.nrg.xnatx.plugins.jupyterhub.initialize; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.constants.Scope; +import org.nrg.jobtemplates.models.*; +import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; +import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; + +/** + * Initialization task for creating the default JupyterHub ComputeSpec and Hardware configurations. + */ +@Component +@Slf4j +public class JupyterHubEnvironmentsAndHardwareInitializer extends AbstractInitializingTask { + + private final XFTManagerHelper xftManagerHelper; + private final XnatAppInfo appInfo; + private final ComputeSpecConfigService computeSpecConfigService; + private final HardwareConfigService hardwareConfigService; + + @Autowired + public JupyterHubEnvironmentsAndHardwareInitializer(final XFTManagerHelper xftManagerHelper, + final XnatAppInfo appInfo, + final ComputeSpecConfigService computeSpecConfigService, + final HardwareConfigService hardwareConfigService) { + this.xftManagerHelper = xftManagerHelper; + this.appInfo = appInfo; + this.computeSpecConfigService = computeSpecConfigService; + this.hardwareConfigService = hardwareConfigService; + } + + @Override + public String getTaskName() { + return "JupyterHubJobTemplateInitializer"; + } + + /** + * Builds the default ComputeSpec and Hardware configurations for JupyterHub. + * @throws InitializingTaskException if the XFT or XNAT services are not initialized. + */ + @Override + protected void callImpl() throws InitializingTaskException { + log.debug("Initializing ComputeSpecs and Hardware for JupyterHub."); + + // Not sure if I need to check both of these or just one. + if (!xftManagerHelper.isInitialized()) { + log.debug("XFT not initialized, deferring execution."); + throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); + } + + if (!appInfo.isInitialized()) { + log.debug("XNAT not initialized, deferring execution."); + throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); + } + + if (computeSpecConfigService.getByType(ComputeSpecConfig.ConfigType.JUPYTERHUB).size() > 0) { + log.debug("ComputeSpecs already exist. Skipping initialization."); + return; + } + + if (hardwareConfigService.retrieveAll().size() > 0) { + log.debug("Hardware already exists. Skipping initialization."); + return; + } + + ComputeSpecConfig computeSpecConfig = buildDefaultComputeSpecConfig(); + + try { + computeSpecConfigService.create(computeSpecConfig); + log.info("Created default ComputeSpec for JupyterHub."); + } catch (Exception e) { + log.error("Error creating default ComputeSpec for JupyterHub.", e); + } + + HardwareConfig smallHardwareConfig = buildSmallHardwareConfig(); + HardwareConfig mediumHardwareConfig = buildMediumHardwareConfig(); + HardwareConfig largeHardwareConfig = buildLargeHardwareConfig(); + HardwareConfig xlargeHardwareConfig = buildXLargeHardwareConfig(); + + try { + hardwareConfigService.create(smallHardwareConfig); + hardwareConfigService.create(mediumHardwareConfig); + hardwareConfigService.create(largeHardwareConfig); + hardwareConfigService.create(xlargeHardwareConfig); + log.info("Created default Hardware for JupyterHub."); + } catch (Exception e) { + log.error("Error creating default Hardware for JupyterHub.", e); + } + } + + /** + * Builds the default ComputeSpec for JupyterHub. + * @return the default ComputeSpec for JupyterHub. + */ + private ComputeSpecConfig buildDefaultComputeSpecConfig() { + // Initialize the ComputeSpecConfig + ComputeSpec computeSpec = new ComputeSpec(); + Map scopes = new HashMap<>(); + ComputeSpecHardwareOptions hardwareOptions = new ComputeSpecHardwareOptions(); + ComputeSpecConfig computeSpecConfig = ComputeSpecConfig.builder() + .id(null) + .configTypes(new HashSet<>(Collections.singletonList(ComputeSpecConfig.ConfigType.JUPYTERHUB))) + .computeSpec(computeSpec) + .scopes(scopes) + .hardwareOptions(hardwareOptions) + .build(); + + // Set the ComputeSpec values + computeSpec.setName("Jupyter Datascience Notebook"); + computeSpec.setImage("jupyter/datascience-notebook:hub-3.0.0"); + computeSpec.setEnvironmentVariables(new ArrayList<>()); + computeSpec.setMounts(new ArrayList<>()); + + // Set the ComputeSpecHardwareOptions values + hardwareOptions.setAllowAllHardware(true); + hardwareOptions.setHardwareConfigs(new HashSet<>()); + + // Set the ComputeSpecScope values + scopes.put(Scope.Site, new ComputeSpecScope(Scope.Site, true, Collections.emptySet())); + scopes.put(Scope.Project, new ComputeSpecScope(Scope.Project, true, Collections.emptySet())); + scopes.put(Scope.User, new ComputeSpecScope(Scope.User, true, Collections.emptySet())); + + return computeSpecConfig; + } + + /** + * Builds the small HardwareConfig for JupyterHub. + * @return the small HardwareConfig for JupyterHub. + */ + private HardwareConfig buildSmallHardwareConfig() { + // Initialize the HardwareConfig + Hardware hardware = new Hardware(); + Map scopes = new HashMap<>(); + HardwareConfig hardwareConfig = HardwareConfig.builder() + .id(null) + .hardware(hardware) + .scopes(scopes) + .build(); + + // Set the Hardware values + hardware.setName("Small"); + hardware.setCpuReservation(2.0); + hardware.setCpuLimit(2.0); + hardware.setMemoryReservation("2G"); + hardware.setMemoryLimit("2G"); + + // Set the HardwareScope values + scopes.put(Scope.Site, new HardwareScope(Scope.Site, true, Collections.emptySet())); + scopes.put(Scope.Project, new HardwareScope(Scope.Project, true, Collections.emptySet())); + scopes.put(Scope.User, new HardwareScope(Scope.User, true, Collections.emptySet())); + + return hardwareConfig; + } + + /** + * Builds the large HardwareConfig for JupyterHub. + * @return the large HardwareConfig for JupyterHub. + */ + private HardwareConfig buildMediumHardwareConfig() { + // Initialize the HardwareConfig + Hardware hardware = new Hardware(); + Map scopes = new HashMap<>(); + HardwareConfig hardwareConfig = HardwareConfig.builder() + .id(null) + .hardware(hardware) + .scopes(scopes) + .build(); + + // Set the Hardware values + hardware.setName("Medium"); + hardware.setCpuReservation(2.0); + hardware.setCpuLimit(2.0); + hardware.setMemoryReservation("4G"); + hardware.setMemoryLimit("4G"); + + // Set the HardwareScope values + scopes.put(Scope.Site, new HardwareScope(Scope.Site, true, Collections.emptySet())); + scopes.put(Scope.Project, new HardwareScope(Scope.Project, true, Collections.emptySet())); + scopes.put(Scope.User, new HardwareScope(Scope.User, true, Collections.emptySet())); + + return hardwareConfig; + } + + /** + * Builds the large HardwareConfig for JupyterHub. + * @return the large HardwareConfig for JupyterHub. + */ + private HardwareConfig buildLargeHardwareConfig() { + // Initialize the HardwareConfig + Hardware hardware = new Hardware(); + Map scopes = new HashMap<>(); + HardwareConfig hardwareConfig = HardwareConfig.builder() + .id(null) + .hardware(hardware) + .scopes(scopes) + .build(); + + // Set the Hardware values + hardware.setName("Large"); + hardware.setCpuReservation(2.0); + hardware.setCpuLimit(2.0); + hardware.setMemoryReservation("8G"); + hardware.setMemoryLimit("8G"); + + // Set the HardwareScope values + scopes.put(Scope.Site, new HardwareScope(Scope.Site, true, Collections.emptySet())); + scopes.put(Scope.Project, new HardwareScope(Scope.Project, true, Collections.emptySet())); + scopes.put(Scope.User, new HardwareScope(Scope.User, true, Collections.emptySet())); + + return hardwareConfig; + } + + /** + * Builds the xlarge HardwareConfig for JupyterHub. + * @return the xlarge HardwareConfig for JupyterHub. + */ + private HardwareConfig buildXLargeHardwareConfig() { + // Initialize the HardwareConfig + Hardware hardware = new Hardware(); + Map scopes = new HashMap<>(); + HardwareConfig hardwareConfig = HardwareConfig.builder() + .id(null) + .hardware(hardware) + .scopes(scopes) + .build(); + + // Set the Hardware values + hardware.setName("XLarge"); + hardware.setCpuReservation(4.0); + hardware.setCpuLimit(4.0); + hardware.setMemoryReservation("16G"); + hardware.setMemoryLimit("16G"); + + // Set the HardwareScope values + scopes.put(Scope.Site, new HardwareScope(Scope.Site, true, Collections.emptySet())); + scopes.put(Scope.Project, new HardwareScope(Scope.Project, true, Collections.emptySet())); + scopes.put(Scope.User, new HardwareScope(Scope.User, true, Collections.emptySet())); + + return hardwareConfig; + } +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java deleted file mode 100644 index 53346d2..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializer.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.initialize; - -import lombok.extern.slf4j.Slf4j; -import org.nrg.xapi.exceptions.DataFormatException; -import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; -import org.nrg.xnat.initialization.tasks.InitializingTaskException; -import org.nrg.xnatx.plugins.jupyterhub.models.Profile; -import org.nrg.xnatx.plugins.jupyterhub.models.docker.ContainerSpec; -import org.nrg.xnatx.plugins.jupyterhub.models.docker.Placement; -import org.nrg.xnatx.plugins.jupyterhub.models.docker.Resources; -import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; -import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.Collections; - -@Component -@Slf4j -public class JupyterHubProfileInitializer extends AbstractInitializingTask { - - private final ProfileService profileService; - private final XFTManagerHelper xfmManagerHelper; - - @Autowired - public JupyterHubProfileInitializer(final ProfileService profileService, - final XFTManagerHelper xfmManagerHelper) { - this.profileService = profileService; - this.xfmManagerHelper = xfmManagerHelper; - } - - @Override - public String getTaskName() { - return "JupyterHubProfileInitializer"; - } - - @Override - protected void callImpl() throws InitializingTaskException { - log.debug("Initializing JupyterHub profile."); - - if (!xfmManagerHelper.isInitialized()) { - log.debug("XFT not initialized, deferring execution."); - throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); - } - - if (profileService.getAll().size() > 0) { - log.debug("JupyterHub profile already exists. Skipping initialization."); - return; - } - - Profile profile = buildDefaultProfile(); - - try { - profileService.create(profile); - log.info("Created default JupyterHub profile."); - } catch (DataFormatException e) { - log.error("Error creating default JupyterHub profile.", e); - } - } - - private Profile buildDefaultProfile() { - ContainerSpec containerSpec = ContainerSpec.builder() - .image("jupyter/datascience-notebook:hub-3.0.0") - .mounts(Collections.emptyList()) - .env(Collections.emptyMap()) - .labels(Collections.emptyMap()) - .build(); - - Placement placement = Placement.builder() - .constraints(Collections.emptyList()) - .build(); - - Resources resources = Resources.builder() - .cpuLimit(null) - .cpuReservation(null) - .memLimit(null) - .memReservation(null) - .genericResources(Collections.emptyMap()) - .build(); - - TaskTemplate taskTemplate = TaskTemplate.builder() - .containerSpec(containerSpec) - .placement(placement) - .resources(resources) - .build(); - - return Profile.builder() - .name("Default") - .description("Default JupyterHub profile.") - .taskTemplate(taskTemplate) - .build(); - } - -} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Profile.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Profile.java deleted file mode 100644 index 85e8c69..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Profile.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.models; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; -import lombok.extern.slf4j.Slf4j; -import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; - -import static com.fasterxml.jackson.annotation.JsonInclude.Include.ALWAYS; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -@EqualsAndHashCode(callSuper = false) -@Slf4j -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(ALWAYS) -public class Profile { - public static final String SWARM_SPAWNER = "dockerspawner.SwarmSpawner"; - public static final String KUBE_SPAWNER = "NOT_IMPLEMENTED"; - - private Long id; - private String name; - private String description; - private String spawner; - private Boolean enabled; - @JsonProperty("task_template") private TaskTemplate taskTemplate; - -} \ No newline at end of file diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java new file mode 100644 index 0000000..ca1be21 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java @@ -0,0 +1,28 @@ +package org.nrg.xnatx.plugins.jupyterhub.models; + +import io.swagger.annotations.ApiModel; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.nrg.xnatx.plugins.jupyterhub.client.models.UserOptions; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Slf4j +@ApiModel(value = "Jupyter Notebook Server Start Request", description = "Jupyter Notebook Server Start Request") +public class ServerStartRequest implements UserOptions { + + private String username; + private String servername; + private String xsiType; + private String itemId; + private String itemLabel; + private String projectId; + private String eventTrackingId; + + private Long computeSpecConfigId; + private Long hardwareConfigId; + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/ProfileDao.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/ProfileDao.java deleted file mode 100644 index e444c35..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/ProfileDao.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.repositories; - -import lombok.extern.slf4j.Slf4j; -import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; -import org.nrg.xnatx.plugins.jupyterhub.entities.ProfileEntity; -import org.springframework.stereotype.Repository; - -@Repository -@Slf4j -public class ProfileDao extends AbstractHibernateDAO { - -} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java index ade3e72..0740e99 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java @@ -2,6 +2,7 @@ import io.swagger.annotations.*; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.nrg.framework.annotations.XapiRestController; import org.nrg.xapi.exceptions.NotFoundException; import org.nrg.xapi.rest.*; @@ -18,11 +19,13 @@ import org.nrg.xnatx.plugins.jupyterhub.client.models.Server; import org.nrg.xnatx.plugins.jupyterhub.client.models.Token; import org.nrg.xnatx.plugins.jupyterhub.client.models.User; +import org.nrg.xnatx.plugins.jupyterhub.models.ServerStartRequest; import org.nrg.xnatx.plugins.jupyterhub.models.XnatUserOptions; import org.nrg.xnatx.plugins.jupyterhub.services.JupyterHubService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -138,16 +141,12 @@ public Server getNamedServer(@ApiParam(value = "username", required = true) @Pat @ApiResponse(code = 500, message = "Unexpected error")}) @XapiRequestMapping(value = "/users/{username}/server", method = POST, restrictTo = Authorizer) @AuthDelegate(JupyterUserAuthorization.class) - public void startServer(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, - @ApiParam(value = "xsiType", required = true) @RequestParam("xsiType") final String xsiType, - @ApiParam(value = "itemId", required = true) @RequestParam("itemId") final String itemId, - @ApiParam(value = "itemLabel", required = true) @RequestParam("itemLabel") final String itemLabel, - @ApiParam(value = "projectId", required = false) @RequestParam(value = "projectId", required = false) final String projectId, - @ApiParam(value = "eventTrackingId", required = true) @RequestParam(value = "eventTrackingId") final String eventTrackingId, - @ApiParam(value = "profileId", required = true) @RequestParam(value = "profileId") final Long profileId) throws UserNotFoundException, UserInitException { - jupyterHubService.startServer(getUserI(username), xsiType, itemId, itemLabel, projectId, eventTrackingId, profileId); + public void startServer(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, // Unused, but required for auth + @RequestBody final ServerStartRequest serverStartRequest) { + jupyterHubService.startServer(getSessionUser(), serverStartRequest); } + @ApiOperation(value = "Starts a Jupyter server for the user", notes = "Use the Event Tracking API to track progress.", hidden = true) @@ -157,15 +156,14 @@ public void startServer(@ApiParam(value = "username", required = true) @PathVari @ApiResponse(code = 500, message = "Unexpected error")}) @XapiRequestMapping(value = "/users/{username}/server/{servername}", method = POST, restrictTo = Authorizer) @AuthDelegate(JupyterUserAuthorization.class) - public void startNamedServer(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, + public void startNamedServer(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, // Unused, but required for auth @ApiParam(value = "servername", required = true) @PathVariable("servername") final String servername, - @ApiParam(value = "xsiType", required = true) @RequestParam("xsiType") final String xsiType, - @ApiParam(value = "itemId", required = true) @RequestParam("itemId") final String itemId, - @ApiParam(value = "itemLabel", required = true) @RequestParam("itemLabel") final String itemLabel, - @ApiParam(value = "projectId", required = false) @RequestParam(value = "projectId", required = false) final String projectId, - @ApiParam(value = "eventTrackingId", required = true) @RequestParam(value = "eventTrackingId") final String eventTrackingId, - @ApiParam(value = "profileId", required = true) @RequestParam(value = "profileId") final Long profileId) throws UserNotFoundException, UserInitException { - jupyterHubService.startServer(getUserI(username), servername, xsiType, itemId, itemLabel, projectId, eventTrackingId, profileId); + @RequestBody final ServerStartRequest serverStartRequest) { + if (!StringUtils.equals(servername, serverStartRequest.getServername())) { + throw new IllegalArgumentException("Server name in path does not match server name in request body."); + } + + jupyterHubService.startServer(getSessionUser(), serverStartRequest); } @ApiOperation(value = "Returns the last known user options for the default server", response = XnatUserOptions.class) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApi.java deleted file mode 100644 index a86cb33..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApi.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.rest; - -import io.swagger.annotations.*; -import lombok.extern.slf4j.Slf4j; -import org.nrg.framework.annotations.XapiRestController; -import org.nrg.xapi.exceptions.DataFormatException; -import org.nrg.xapi.exceptions.NotFoundException; -import org.nrg.xapi.rest.AbstractXapiRestController; -import org.nrg.xapi.rest.XapiRequestMapping; -import org.nrg.xdat.security.helpers.AccessLevel; -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xnatx.plugins.jupyterhub.models.Profile; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; - -import java.util.Comparator; -import java.util.List; - -import static java.util.stream.Collectors.toList; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import static org.springframework.web.bind.annotation.RequestMethod.*; - - -@Api("JupyterHub Profiles API") -@XapiRestController -@RequestMapping("/jupyterhub/profiles") -@Slf4j -public class JupyterHubProfilesApi extends AbstractXapiRestController { - - private final ProfileService profileService; - - @Autowired - public JupyterHubProfilesApi(final UserManagementServiceI userManagementService, - final RoleHolder roleHolder, - final ProfileService profileService) { - super(userManagementService, roleHolder); - this.profileService = profileService; - } - - @ApiOperation(value = "Get a profiles.", response = Profile.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Profile successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = AccessLevel.Authenticated) - public Profile get(@ApiParam(value = "id", required = true) @PathVariable final Long id) throws NotFoundException { - return profileService.get(id).orElseThrow(() -> new NotFoundException("No profile with id " + id + " exists.")); - } - - @ApiOperation(value = "Get all profiles.", response = Profile.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Profiles successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = AccessLevel.Authenticated) - public List getAll() { - return profileService.getAll() - .stream() - .sorted(Comparator.comparing(Profile::getName)).collect(toList()); - } - - @ApiOperation(value = "Create a profile.") - @ApiResponses({ - @ApiResponse(code = 201, message = "Profile successfully created."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @ResponseStatus(HttpStatus.CREATED) - @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = POST, restrictTo = AccessLevel.Admin) - public Long create(@ApiParam(value = "profile", required = true) @RequestBody final Profile profile) throws DataFormatException { - return profileService.create(profile); - } - - @ApiOperation(value = "Update a profile.") - @ApiResponses({ - @ApiResponse(code = 200, message = "Profile successfully updated."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = PUT, restrictTo = AccessLevel.Admin) - public void update(@ApiParam(value = "id", required = true) @PathVariable final Long id, - @ApiParam(value = "profile", required = true) @RequestBody final Profile profile) throws NotFoundException, DataFormatException { - if (!id.equals(profile.getId())) { - throw new DataFormatException("The profile ID in the path must match the ID in the body."); - } else if (!profileService.exists(id)) { - throw new NotFoundException("No profile with id " + id + " exists."); - } - - profileService.update(profile); - } - - @ApiOperation(value = "Delete a profile.") - @ApiResponses({ - @ApiResponse(code = 200, message = "Profile successfully deleted."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = DELETE, restrictTo = AccessLevel.Admin) - public void delete(@ApiParam(value = "id", required = true) @PathVariable final Long id) throws NotFoundException { - if (!profileService.exists(id)) { - throw new NotFoundException("No profile with id " + id + " exists."); - } - - profileService.delete(id); - } - -} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java index bfe8870..795618e 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java @@ -5,6 +5,7 @@ import org.nrg.xnatx.plugins.jupyterhub.client.models.Server; import org.nrg.xnatx.plugins.jupyterhub.client.models.Token; import org.nrg.xnatx.plugins.jupyterhub.client.models.User; +import org.nrg.xnatx.plugins.jupyterhub.models.ServerStartRequest; import java.util.List; import java.util.Optional; @@ -18,8 +19,7 @@ public interface JupyterHubService { List getUsers(); Optional getServer(UserI user); Optional getServer(UserI user, String servername); - void startServer(UserI user, String xsiType, String itemId, String itemLabel, String projectId, String eventTrackingId, Long profileId); - void startServer(UserI user, String servername, String xsiType, String itemId, String itemLabel, String projectId, String eventTrackingId, Long profileId); + void startServer(UserI user, ServerStartRequest startRequest); void stopServer(UserI user, String eventTrackingId); void stopServer(UserI user, String servername, String eventTrackingId); Token createToken(UserI user, String note, Integer expiresIn); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileEntityService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileEntityService.java deleted file mode 100644 index b4ed6d9..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileEntityService.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.services; - -import org.nrg.framework.orm.hibernate.BaseHibernateService; -import org.nrg.xnatx.plugins.jupyterhub.entities.ProfileEntity; - -public interface ProfileEntityService extends BaseHibernateService { - -} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileService.java deleted file mode 100644 index 96218ae..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/ProfileService.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.services; - -import org.nrg.xapi.exceptions.DataFormatException; -import org.nrg.xnatx.plugins.jupyterhub.models.Profile; - -import java.util.List; -import java.util.Optional; - -public interface ProfileService { - - boolean exists(Long id); - Long create(Profile profile) throws DataFormatException; - Optional get(Long id); - List getAll(); - void update(Profile profile) throws DataFormatException; - void delete(Long id); - -} \ No newline at end of file diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java index 110af9f..ecaa250 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java @@ -20,6 +20,6 @@ public interface UserOptionsService { Optional retrieveUserOptions(UserI user); Optional retrieveUserOptions(UserI user, String servername); - void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, Long profileId, String eventTrackingId); + void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, Long computeSpecConfigId, Long hardwareConfigId, String eventTrackingId); } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java index d287e8c..8360ca8 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java @@ -1,7 +1,9 @@ package org.nrg.xnatx.plugins.jupyterhub.services.impl; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.nrg.framework.services.NrgEventServiceI; +import org.nrg.jobtemplates.services.JobTemplateService; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xdat.security.user.exceptions.UserInitException; import org.nrg.xft.security.UserI; @@ -14,23 +16,19 @@ import org.nrg.xnatx.plugins.jupyterhub.client.models.User; import org.nrg.xnatx.plugins.jupyterhub.events.JupyterServerEvent; import org.nrg.xnatx.plugins.jupyterhub.events.JupyterServerEventI; +import org.nrg.xnatx.plugins.jupyterhub.models.ServerStartRequest; import org.nrg.xnatx.plugins.jupyterhub.models.XnatUserOptions; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; import org.nrg.xnatx.plugins.jupyterhub.services.JupyterHubService; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import javax.annotation.Nullable; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.CompletableFuture; @SuppressWarnings("BusyWait") @@ -44,7 +42,7 @@ public class DefaultJupyterHubService implements JupyterHubService { private final UserOptionsService userOptionsService; private final JupyterHubPreferences jupyterHubPreferences; private final UserManagementServiceI userManagementService; - private final ProfileService profileService; + private final JobTemplateService jobTemplateService; @Autowired public DefaultJupyterHubService(final JupyterHubClient jupyterHubClient, @@ -53,14 +51,14 @@ public DefaultJupyterHubService(final JupyterHubClient jupyterHubClient, final UserOptionsService userOptionsService, final JupyterHubPreferences jupyterHubPreferences, final UserManagementServiceI userManagementService, - final ProfileService profileService) { + final JobTemplateService jobTemplateService) { this.jupyterHubClient = jupyterHubClient; this.eventService = eventService; this.permissionsHelper = permissionsHelper; this.userOptionsService = userOptionsService; this.jupyterHubPreferences = jupyterHubPreferences; this.userManagementService = userManagementService; - this.profileService = profileService; + this.jobTemplateService = jobTemplateService; } /** @@ -139,45 +137,24 @@ public Optional getServer(final UserI user, final String servername) { } /** - * Asynchronously start the default Jupyter notebook server for the user. The provided entity's resources will be - * mounted to the Jupyter notebook server container. The progress of the server launch is tracked with the event - * tracking api. - * - * @param user User to start the server for. - * @param xsiType Accepts xnat:projectData, xnat:subjectData, xnat:experimentData and its children, and - * xdat:stored_search - * @param itemId The provided id's resources will be mounted to the Jupyter notebook server container - * @param itemLabel The label of the provided item (e.g. the subject label or experiment label). - * @param projectId Can be null for xdat:stored_search - * @param eventTrackingId Use with the event tracking api to track progress. - * @param profileId The id of the profile to use for this server. + * Asynchronously starts a Jupyter notebook server based on the provided request. Use the event tracking api to keep + * track of progress. + * @param user The user requesting the server. + * @param startRequest The request to start a Jupyter notebook server. */ @Override - public void startServer(final UserI user, final String xsiType, final String itemId, - final String itemLabel, @Nullable final String projectId, final String eventTrackingId, - final Long profileId) { - startServer(user, "", xsiType, itemId, itemLabel, projectId, eventTrackingId, profileId); - } + public void startServer(final UserI user, final ServerStartRequest startRequest) { + validateServerStartRequest(user, startRequest); + + final String servername = startRequest.getServername(); + final String xsiType = startRequest.getXsiType(); + final String itemId = startRequest.getItemId(); + final String itemLabel = startRequest.getItemLabel(); + final String projectId = startRequest.getProjectId(); + final String eventTrackingId = startRequest.getEventTrackingId(); + final Long computeSpecConfigId = startRequest.getComputeSpecConfigId(); + final Long hardwareConfigId = startRequest.getHardwareConfigId(); - /** - * Asynchronously starts a named Jupyter notebook server for the user. The provided entity's resources will be - * mounted to the Jupyter notebook server container. The progress of the server launch is tracked with the event - * tracking api. - * - * @param user User to start the server for. - * @param servername The name of the server that will be started - * @param xsiType Accepts xnat:projectData, xnat:subjectData, xnat:experimentData and its children, and - * xdat:stored_search - * @param itemId The provided id's resources will be mounted to the Jupyter notebook server container - * @param itemLabel The label of the provided item (e.g. the subject label or experiment label). - * @param projectId Can be null for xdat:stored_search - * @param eventTrackingId Use with the event tracking api to track progress. - * @param profileId The id of the profile to use for this server. - */ - @Override - public void startServer(final UserI user, String servername, final String xsiType, final String itemId, - final String itemLabel, @Nullable final String projectId, final String eventTrackingId, - final Long profileId) { eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, 0, "Starting Jupyter notebook server for user " + user.getUsername())); @@ -189,10 +166,10 @@ public void startServer(final UserI user, String servername, final String xsiTyp return; } - if (!profileService.exists(profileId)) { + if (!jobTemplateService.isAvailable(user.getUsername(), projectId, computeSpecConfigId, hardwareConfigId)) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Failed to launch Jupyter notebook server. Profile does not exist.")); + "Failed to launch Jupyter notebook server. The job template either does not exist or is not available to the user and project.")); return; } @@ -230,7 +207,7 @@ public void startServer(final UserI user, String servername, final String xsiTyp JupyterServerEventI.Operation.Start, 20, "Building notebook server container configuration.")); - userOptionsService.storeUserOptions(user, servername, xsiType, itemId, projectId, profileId, eventTrackingId); + userOptionsService.storeUserOptions(user, servername, xsiType, itemId, projectId, computeSpecConfigId, hardwareConfigId, eventTrackingId); eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, 30, @@ -302,6 +279,64 @@ public void startServer(final UserI user, String servername, final String xsiTyp }); } + /** + * Validates the provided request. Throws an exception if the request is invalid. + * @param user The user making the request. + * @param request The request to validate. + */ + public void validateServerStartRequest(UserI user, ServerStartRequest request) { + if (user == null) { + throw new IllegalArgumentException("User cannot be null"); + } + + if (request == null) { + throw new IllegalArgumentException("ServerStartRequest cannot be null"); + } + + List errorMessages = new ArrayList<>(); + + if (!StringUtils.equals(user.getUsername(), request.getUsername())) { + errorMessages.add("Usernames do not match"); + } + + if (StringUtils.isBlank(request.getUsername())) { + errorMessages.add("Username cannot be blank"); + } + + if (StringUtils.isBlank(request.getXsiType())) { + errorMessages.add("XSI type cannot be blank"); + } + + if (StringUtils.isBlank(request.getItemId())) { + errorMessages.add("Item ID cannot be blank"); + } + + if (StringUtils.isBlank(request.getItemLabel())) { + errorMessages.add("Item label cannot be blank"); + } + + if (StringUtils.isBlank(request.getProjectId())) { + errorMessages.add("Project ID cannot be blank"); + } + + if (StringUtils.isBlank(request.getEventTrackingId())) { + errorMessages.add("Event tracking ID cannot be blank"); + } + + if (request.getComputeSpecConfigId() == null) { + errorMessages.add("Compute spec config ID cannot be null"); + } + + if (request.getHardwareConfigId() == null) { + errorMessages.add("Compute resource cannot be null"); + } + + if (!errorMessages.isEmpty()) { + throw new IllegalArgumentException(String.join(", ", errorMessages)); + } + } + + /** * Asynchronously stops the default unnamed Jupyter notebook server for the provided user. Use the event tracking * api to keep track of progress. diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileService.java deleted file mode 100644 index 8a2b1db..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileService.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.services.impl; - -import lombok.extern.slf4j.Slf4j; -import org.nrg.xapi.exceptions.DataFormatException; -import org.nrg.xnatx.plugins.jupyterhub.entities.ProfileEntity; -import org.nrg.xnatx.plugins.jupyterhub.models.Profile; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileEntityService; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -/** - * Default implementation of the profile service. Valid profiles must have a name, description, task template, container - * spec, and image defined. - */ -@Service -@Slf4j -public class DefaultProfileService implements ProfileService { - - private final ProfileEntityService profileEntityService; - - @Autowired - public DefaultProfileService(final ProfileEntityService profileEntityService) { - this.profileEntityService = profileEntityService; - } - - /** - * Check if a profile exists. - * @param id profile id - * @return true if the profile exists, false otherwise - */ - @Override - public boolean exists(final Long id) { - final ProfileEntity profileEntity = profileEntityService.retrieve(id); - return profileEntity != null; - } - - /** - * Create a profile. - * @param profile profile to create - * @return id of the created profile - * - * @throws DataFormatException if the profile is invalid - */ - @Override - public Long create(final Profile profile) throws DataFormatException { - profile.setId(null); - - // Validate the profile - validate(profile); - - final ProfileEntity profileEntity = ProfileEntity.fromPojo(profile); - ProfileEntity created = profileEntityService.create(profileEntity); - created.getProfile().setId(created.getId()); // Update the id in the profile - profileEntityService.update(created); - return created.getId(); - } - - /** - * Get a profile. - * @param id profile id - * @return optional profile if it exists - */ - @Override - public Optional get(final Long id) { - Optional profileEntity = Optional.ofNullable(profileEntityService.retrieve(id)); - return profileEntity.map(ProfileEntity::toPojo); - } - - /** - * Get all profiles. - * @return list of profiles - */ - @Override - public List getAll() { - return profileEntityService.getAll() - .stream() - .map(ProfileEntity::toPojo) - .collect(Collectors.toList()); - } - - /** - * Update an existing profile. - * @param toUpdate profile to update - * - * @throws DataFormatException if the profile is invalid - */ - @Override - public void update(Profile toUpdate) throws DataFormatException { - final ProfileEntity profileEntity = profileEntityService.retrieve(toUpdate.getId()); - - // Profile not found - if (profileEntity == null) { - DataFormatException exception = new DataFormatException(); - exception.addInvalidField("id", "Profile not found"); - throw exception; - } - - // Validate the profile - validate(toUpdate); - - final ProfileEntity template = ProfileEntity.fromPojo(toUpdate); - final ProfileEntity updated = profileEntity.update(template); - - profileEntityService.update(updated); - } - - /** - * Delete a profile. - * @param id profile id - */ - @Override - public void delete(final Long id) { - profileEntityService.delete(id); - } - - /** - * Validate a profile. Profile is invalid if: - * - name is empty - * - description is empty - * - task template is empty - * - container spec is empty - * - image is empty - * - * @param profile profile to validate - * @throws DataFormatException if the profile is invalid - */ - protected void validate(final Profile profile) throws DataFormatException { - DataFormatException exception = new DataFormatException(); - - if (profile.getName() == null || profile.getName().isEmpty()) { - exception.addInvalidField('"' + "name" + '"' + " cannot be empty"); - } - - if (profile.getDescription() == null || profile.getDescription().isEmpty()) { - exception.addInvalidField('"' + "description" + '"' + " cannot be empty"); - } - - if (profile.getTaskTemplate() == null) { - exception.addInvalidField('"' + "taskTemplate" + '"' + " cannot be empty"); - throw exception; - } - - if (profile.getTaskTemplate().getContainerSpec() == null) { - exception.addInvalidField('"' + "containerSpec" + '"' + " cannot be empty"); - throw exception; - } - - if (profile.getTaskTemplate().getContainerSpec().getImage() == null || profile.getTaskTemplate().getContainerSpec().getImage().isEmpty()) { - exception.addInvalidField('"' + "image" + '"' + " cannot be empty"); - } - - if (exception.hasDataFormatErrors()) { - throw exception; - } - } - -} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java index 294430f..12704f9 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java @@ -2,6 +2,9 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import lombok.NonNull; +import org.nrg.jobtemplates.models.*; +import org.nrg.jobtemplates.services.JobTemplateService; import org.nrg.xdat.entities.AliasToken; import org.nrg.xdat.model.XnatSubjectassessordataI; import org.nrg.xdat.om.*; @@ -16,10 +19,10 @@ import org.nrg.xft.security.UserI; import org.nrg.xnat.exceptions.InvalidArchiveStructure; import org.nrg.xnatx.plugins.jupyterhub.entities.UserOptionsEntity; -import org.nrg.xnatx.plugins.jupyterhub.models.*; +import org.nrg.xnatx.plugins.jupyterhub.models.XnatUserOptions; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.Mount; import org.nrg.xnatx.plugins.jupyterhub.models.docker.*; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsEntityService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.services.UserWorkspaceService; @@ -45,7 +48,7 @@ public class DefaultUserOptionsService implements UserOptionsService { private final SiteConfigPreferences siteConfigPreferences; private final UserOptionsEntityService userOptionsEntityService; private final PermissionsHelper permissionsHelper; - private final ProfileService profileService; + private final JobTemplateService jobTemplateService; @Autowired public DefaultUserOptionsService(final JupyterHubPreferences jupyterHubPreferences, @@ -55,7 +58,7 @@ public DefaultUserOptionsService(final JupyterHubPreferences jupyterHubPreferenc final SiteConfigPreferences siteConfigPreferences, final UserOptionsEntityService userOptionsEntityService, final PermissionsHelper permissionsHelper, - final ProfileService profileService) { + final JobTemplateService jobTemplateService) { this.jupyterHubPreferences = jupyterHubPreferences; this.userWorkspaceService = userWorkspaceService; this.searchHelperService = searchHelperService; @@ -63,7 +66,7 @@ public DefaultUserOptionsService(final JupyterHubPreferences jupyterHubPreferenc this.siteConfigPreferences = siteConfigPreferences; this.userOptionsEntityService = userOptionsEntityService; this.permissionsHelper = permissionsHelper; - this.profileService = profileService; + this.jobTemplateService = jobTemplateService; } @Override @@ -270,7 +273,7 @@ public Optional retrieveUserOptions(UserI user, String serverna @Override public void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, - Long profileId, String eventTrackingId) { + Long computeSpecConfigId, Long hardwareConfigId, String eventTrackingId) { log.debug("Storing user options for user '{}' server '{}' xsiType '{}' id '{}' projectId '{}'", user.getUsername(), servername, xsiType, id, projectId); @@ -278,12 +281,8 @@ public void storeUserOptions(UserI user, String servername, String xsiType, Stri return; } - Optional profileOpt = profileService.get(profileId); - if (!profileOpt.isPresent()) { - return; - } - - Profile profile = profileOpt.get(); + // Resolve the job template before resolving the paths + JobTemplate jobTemplate = jobTemplateService.resolve(user.getUsername(), projectId, computeSpecConfigId, hardwareConfigId); // specific xsi type -> general xsi type if (instanceOf(xsiType, XnatExperimentdata.SCHEMA_ELEMENT_NAME)) { @@ -344,7 +343,7 @@ public void storeUserOptions(UserI user, String servername, String xsiType, Stri mounts.addAll(xnatDataMounts); // Add mounts - TaskTemplate taskTemplate = profile.getTaskTemplate(); + TaskTemplate taskTemplate = toTaskTemplate(jobTemplate); taskTemplate.getContainerSpec().getMounts().addAll(mounts); // Get and add env variables @@ -406,4 +405,87 @@ private boolean instanceOf(final String xsiType, final String instanceOfXsiType) } } + protected TaskTemplate toTaskTemplate(@NonNull JobTemplate jobTemplate) { + // Setup the TaskTemplate + ContainerSpec containerSpec = ContainerSpec + .builder() + .image("") + .env(new HashMap<>()) + .labels(new HashMap<>()) + .mounts(new ArrayList<>()) + .build(); + + Resources resources = Resources.builder() + .genericResources(new HashMap<>()) + .build(); + + Placement placement = new Placement(new ArrayList<>()); + + TaskTemplate taskTemplate = TaskTemplate.builder() + .containerSpec(containerSpec) + .resources(resources) + .placement(placement) + .build(); + + // Get the compute spec, hardware, and constraints from the job template + ComputeSpec computeSpec = jobTemplate.getComputeSpec(); + Hardware hardware = jobTemplate.getHardware(); + List constraints = jobTemplate.getConstraints(); + + // Build container spec for the task template + containerSpec.setImage(computeSpec.getImage()); + + // Add environment variables from compute spec and hardware + Map environmentVariables = new HashMap<>(); + Map computeSpecEnvVars = computeSpec.getEnvironmentVariables().stream().collect(Collectors.toMap(EnvironmentVariable::getKey, EnvironmentVariable::getValue)); + Map hardwareEnvVars = hardware.getEnvironmentVariables().stream().collect(Collectors.toMap(EnvironmentVariable::getKey, EnvironmentVariable::getValue)); + + // check for empty otherwise putAll will throw an exception + if (!computeSpecEnvVars.isEmpty()) { + environmentVariables.putAll(computeSpecEnvVars); + } + + // check for empty otherwise putAll will throw an exception + if (!hardwareEnvVars.isEmpty()) { + environmentVariables.putAll(hardwareEnvVars); + } + + containerSpec.setEnv(environmentVariables); + + // Add mounts from compute spec and hardware + // check for empty otherwise addAll will throw an exception + List mounts = new ArrayList<>(); + if (!computeSpec.getMounts().isEmpty()) { + mounts.addAll(computeSpec.getMounts().stream().map(mount -> { + return Mount.builder() + .source(mount.getLocalPath()) + .target(mount.getContainerPath()) + .type(StringUtils.isEmpty(mount.getVolumeName()) ? "bind" : "volume") + .readOnly(mount.isReadOnly()) + .build(); + }).collect(Collectors.toList())); + } + + containerSpec.setMounts(mounts); + + // Build resources + resources.setCpuReservation(hardware.getCpuReservation()); + resources.setCpuLimit(hardware.getCpuLimit()); + resources.setMemReservation(hardware.getMemoryReservation()); + resources.setMemLimit(hardware.getMemoryLimit()); + + if (!hardware.getGenericResources().isEmpty()) { + resources.setGenericResources((hardware.getGenericResources().stream().collect(Collectors.toMap(GenericResource::getName, GenericResource::getValue)))); + } + // Build placement + if (!hardware.getConstraints().isEmpty()) { + placement.getConstraints().addAll(hardware.getConstraints().stream().flatMap(constraint -> constraint.toList().stream()).collect(Collectors.toList())); + } + + if (constraints != null && !constraints.isEmpty()) { + placement.getConstraints().addAll(constraints.stream().flatMap(constraint -> constraint.toList().stream()).collect(Collectors.toList())); + } + + return taskTemplate; + } } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateProfileEntityService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateProfileEntityService.java deleted file mode 100644 index e33ad60..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateProfileEntityService.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.services.impl; - - -import lombok.extern.slf4j.Slf4j; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; -import org.nrg.xnatx.plugins.jupyterhub.entities.ProfileEntity; -import org.nrg.xnatx.plugins.jupyterhub.repositories.ProfileDao; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileEntityService; -import org.springframework.stereotype.Service; - -import javax.transaction.Transactional; - -@Service -@Slf4j -@Transactional -public class HibernateProfileEntityService extends AbstractHibernateEntityService implements ProfileEntityService { - -} diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/compute-spec-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/compute-spec-configs.js new file mode 100644 index 0000000..51f3afc --- /dev/null +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/compute-spec-configs.js @@ -0,0 +1,1014 @@ +console.debug("Loading compute-spec-configs.js"); + +var XNAT = getObject(XNAT || {}); +XNAT.app = getObject(XNAT.app || {}); +XNAT.plugin = getObject(XNAT.plugin || {}); +XNAT.plugin.jobTemplates = getObject(XNAT.plugin.jobTemplates|| {}); +XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates.computeSpecConfigs || {}); + +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + return factory(); + } +}(function () { + + XNAT.plugin.jobTemplates.computeSpecConfigs.get = async (id) => { + console.debug("Fetching compute spec config " + id) + const url = XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs/${id}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error(`Error fetching compute spec config ${id}`); + } + } + + XNAT.plugin.jobTemplates.computeSpecConfigs.getAll = async (type) => { + console.debug("Fetching compute spec configs") + + const url = type ? + XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs?type=${type}`) : + XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error(`Error fetching compute spec configs`); + } + } + + XNAT.plugin.jobTemplates.computeSpecConfigs.create = async (computeSpec) => { + console.debug("Creating compute spec config") + const url = XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs`); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(computeSpec) + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error(`Error creating compute spec config`); + } + } + + XNAT.plugin.jobTemplates.computeSpecConfigs.update = async (computeSpec) => { + console.debug("Updating compute spec config") + const id = computeSpec['id']; + + if (!id) { + throw new Error(`Cannot update compute spec config without an ID`); + } + + const url = XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs/${id}`); + + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(computeSpec) + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error(`Error updating compute spec config ${id}`); + } + } + + XNAT.plugin.jobTemplates.computeSpecConfigs.save = async (computeSpec) => { + console.debug("Saving compute spec config") + const id = computeSpec['id']; + + if (id) { + return XNAT.plugin.jobTemplates.computeSpecConfigs.update(computeSpec); + } else { + return XNAT.plugin.jobTemplates.computeSpecConfigs.create(computeSpec); + } + } + + XNAT.plugin.jobTemplates.computeSpecConfigs.delete = async (id) => { + console.debug("Deleting compute spec config " + id) + const url = XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs/${id}`); + + const response = await fetch(url, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error(`Error deleting compute spec config ${id}`); + } + } + + XNAT.plugin.jobTemplates.computeSpecConfigs.available = async (type, user, project) => { + console.debug(`Fetching available compute spec configs for type ${type} and user ${user} and project ${project}`); + const url = type ? + XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs/available?type=${type}&user=${user}&project=${project}`) : + XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs/available?user=${user}&project=${project}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error(`Error fetching available compute spec configs`); + } + } + + XNAT.plugin.jobTemplates.computeSpecConfigs.manager = async (containerId, computeSpecType) => { + console.debug("Initializing compute spec manager") + + let container, + footer, + computeSpecConfigs = [], + hardwareConfigs = [], + users = [], + projects = []; + + computeSpecType = computeSpecType || ''; + + const init = () => { + container = document.getElementById(containerId); + + if (!container) { + throw new Error(`Cannot find container with id ${containerId}`); + } + + clearContainer(); + renderNewButton(); + refreshTable(); + + getUsers().then(u => users = u.filter(u => u !== 'jupyterhub' && u !== 'guest')); + getProjects().then(p => projects = p); + getHardwareConfigs() + } + + const getUsers = async () => { + let response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users`); + + if (response.ok) { + return response.json(); + } else { + throw new Error(`Error fetching users`); + } + } + + const getProjects = async () => { + let response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/data/projects`); + + if (response.ok) { + let projects = await response.json(); + projects = projects.ResultSet?.Result || []; + projects = projects.map(p => p['ID']); + return projects; + } else { + throw new Error(`Error fetching projects`); + } + } + + const getHardwareConfigs = () => { + XNAT.plugin.jobTemplates.hardwareConfigs.getAll().then(h => hardwareConfigs = h); + } + + const clearContainer = () => { + container.innerHTML = ''; + } + + const renderNewButton = () => { + footer = container.closest('.panel').querySelector('.panel-footer'); + footer.innerHTML = ''; + + let button = spawn('div', [ + spawn('div.pull-right', [ + spawn('button.btn.btn-sm.submit', { html: 'New ' + (computeSpecType === 'JUPYTERHUB' ? 'Jupyter Environment' : 'ComputeSpec'), onclick: () => editor(null, 'new') }) + ]), + spawn('div.clear.clearFix') + ]) + + footer.appendChild(button); + } + + const enabledToggle = (config) => { + let enabled = config['scopes']['Site']['enabled']; + let ckbox = spawn('input', { + type: 'checkbox', + checked: enabled, + value: enabled ? 'true' : 'false', + data: {checked: enabled}, + onchange: () => { + let enabled = ckbox.checked; + config['scopes']['Site']['enabled'] = enabled; + + XNAT.plugin.jobTemplates.computeSpecConfigs.update(config).then(() => { + XNAT.ui.banner.top(2000, `Compute Spec ${enabled ? 'Enabled' : 'Disabled'}`, 'success'); + }).catch((err) => { + console.error(err); + XNAT.ui.banner.top(4000, `Error ${enabled ? 'Enabling' : 'Disabling'} Compute Spec`, 'error'); + toggleCheckbox(!enabled); + }); + } + }); + + let toggleCheckbox = (enabled) => { + ckbox.checked = enabled; + ckbox.value = enabled ? 'true' : 'false'; + ckbox.dataset.checked = enabled; + } + + return spawn('label.switchbox', [ckbox, ['span.switchbox-outer', [['span.switchbox-inner']]]]); + } + + const displayUsers = (config) => { + let isAllUsersEnabled = config['scopes']['User']['enabled']; + let users = config['scopes']['User']['ids']; + + if (isAllUsersEnabled) { + return 'All Users'; + } else { + if (users.length > 4) { + function showUserModal(){ + XNAT.dialog.message.open({ + title: 'Enabled Users', + content: '
    • ' + users.join('
    • ') + '
    ', + }); + } + + return spawn('span.show-enabled-users',{ + style: { + 'border-bottom': '1px #ccc dashed', + 'cursor': 'pointer' + }, + data: { 'users': users.join(',') }, + title: 'Click to view users', + onclick: showUserModal + }, + `${users.length} Users Enabled` + ); + } else { + if (users.length === 0) { + return 'No Users Enabled'; + } + + return users.sort().join(', '); + } + } + } + + const displayProjects = (config) => { + let isAllProjectsEnabled = config['scopes']['Project']['enabled']; + let projects = config['scopes']['Project']['ids']; + + if (isAllProjectsEnabled) { + return 'All Projects'; + } else { + if (projects.length > 4) { + function showProjectModal(){ + XNAT.dialog.message.open({ + title: 'Enabled Projects', + content: '
    • ' + projects.join('
    • ') + '
    ', + }); + } + + return spawn('span.show-enabled-projects',{ + style: { + 'border-bottom': '1px #ccc dashed', + 'cursor': 'pointer' + }, + data: { 'projects': projects.join(',') }, + title: 'Click to view projects', + onclick: showProjectModal + }, + `${projects.length} Projects Enabled` + ); + } else { + if (projects.length === 0) { + return 'No Projects Enabled'; + } + + return projects.sort().join(', '); + } + } + } + + const displayHardware = (config) => { + let isAllHardwareEnabled = config['hardwareOptions']['allowAllHardware']; + let hardwareConfigs = config['hardwareOptions']['hardwareConfigs']; + let hardwareConfigNames = hardwareConfigs.map((config) => config['hardware']['name']); + + if (isAllHardwareEnabled) { + return 'All Hardware'; + } else { + if (hardwareConfigs.length > 4) { + function showHardwareModal(){ + XNAT.dialog.message.open({ + title: 'Enabled Hardware', + content: '
    • ' + hardwareConfigNames.join('
    • ') + '
    ', + }); + } + + return spawn('span.show-enabled-hardware',{ + style: { + 'border-bottom': '1px #ccc dashed', + 'cursor': 'pointer' + }, + data: { 'hardware': hardwareConfigNames.join(',') }, + title: 'Click to view hardware', + onclick: showHardwareModal + }, + `${hardwareConfigs.length} Hardware Enabled` + ); + } else { + if (hardwareConfigs.length === 0) { + return 'No Hardware Enabled'; + } + + return hardwareConfigNames.join(', '); + } + } + } + + /** + * Open the editor for a compute spec config + * @param config - the compute spec config to edit + * @param action - the action to perform (new, edit, or copy) + */ + const editor = (config, action) => { + console.debug("Opening compute spec config editor") + let isNew = action === 'new', + isCopy = action === 'copy', + isEdit = action === 'edit', + title = isNew || isCopy ? 'New ' : 'Edit ', + isJupyter = config ? config.configTypes.includes('JUPYTERHUB') : false; + + title = title + (isJupyter ? 'Jupyter Environment' : 'ComputeSpec'); + + + XNAT.dialog.open({ + title: title, + content: spawn('div', { id: 'compute-spec-editor' }), + width: 650, + maxBtn: true, + beforeShow: () => { + const formEl = document.getElementById('compute-spec-editor'); + formEl.classList.add('panel'); + + let id = isNew || isCopy ? '' : config.id; + let type = computeSpecType; + let name = isNew || isCopy ? '' : config.computeSpec?.name || ''; + let image = isNew ? '' : config.computeSpec?.image; + let environmentVariables = isNew ? [] : config.computeSpec?.environmentVariables; + let mounts = isNew ? [] : config.computeSpec?.mounts; + + let siteEnabled = isNew ? true : config.scopes?.Site?.enabled; + let allUsersEnabled = isNew ? true : config.scopes?.User?.enabled; + let enabledUserIds = isNew ? [] : config.scopes?.User?.ids; + let allProjectsEnabled = isNew ? true : config.scopes?.Project?.enabled; + let enabledProjectIds = isNew ? [] : config.scopes?.Project?.ids; + + let allHardwareEnabled = isNew ? true : config.hardwareOptions?.allowAllHardware; + let enabledHardwareConfigs = isNew ? [] : config.hardwareOptions?.hardwareConfigs; + + let idEl = XNAT.ui.panel.input.text({ + name: 'id', + id: 'id', + label: 'ID *', + description: 'The ID', + value: id, + }); + + idEl.style.display = 'none'; + + let typeEl = XNAT.ui.panel.input.text({ + name: 'type', + id: 'type', + label: 'Type *', + description: 'The type (e.g. JupyterHub or Container Service)', + value: type, + }); + + typeEl.style.display = 'none'; + + let nameEl = XNAT.ui.panel.input.text({ + name: 'name', + id: 'name', + label: 'Name *', + description: 'User-friendly display name', + value: name, + }); + + let imageEl = XNAT.ui.panel.input.text({ + name: 'image', + id: 'image', + label: 'Image *', + description: 'The Docker image to use. This should be the full image name including the tag. ' + + 'The image must be available on the Docker host where the container will be run.', + value: image, + }); + + let environmentVariablesHeaderEl = spawn('div.environment-variables', [ + spawn('p', 'Environment Variables
    Use this section to define additional ' + + 'environment variables for the container.'), + ]); + + let addEnvironmentVariableButtonEl = spawn('button.btn.btn-sm.add-environment-variable', { + html: 'Add Environment Variable', + style: { 'margin-top': '0.75em' }, + onclick: () => { + addEnvironmentVariable(); + } + }); + + let mountsEl = spawn('div.mounts', [ + spawn('p', 'Mounts
    Use this section to define additional ' + + 'bind mounts to mount into the container.'), + ]); + + let addMountButton = spawn('button.btn.btn-sm.add-mount', { + html: 'Add Mount', + style: { 'margin-top': '0.75em' }, + onclick: () => { + addMount(); + } + }); + + let userAndProjectScopesDescription = spawn('div.user-and-project-scopes', [ + spawn('p', 'Projects and Users
    Use this section to define which projects ' + + 'and users will have access to this.'), + ]); + + let siteEnabledEl = XNAT.ui.panel.input.checkbox({ + name: 'siteEnabled', + id: 'siteEnabled', + label: 'Site Enabled', + description: 'Enable this ComputeSpec site-wide.', + value: siteEnabled, + }); + + siteEnabledEl.style.display = 'none'; + + let allUsersEnabledEl = XNAT.ui.panel.input.checkbox({ + name: 'allUsersEnabled', + id: 'allUsersEnabled', + label: 'All Users', + description: 'Enable for all users', + value: allUsersEnabled, + }); + + let userOptions = users.map((user) => { + return { + value: user, + label: user, + selected: enabledUserIds.includes(user) + } + }) + + let userSelectEl = XNAT.ui.panel.select.multiple({ + name: 'userIds', + id: 'userIds', + label: 'Users', + description: 'Select the users who can access this', + options: userOptions, + }); + + // Disable the user select if all users are enabled + allUsersEnabledEl.querySelector('input').addEventListener('change', (event) => { + userSelectEl.querySelector('select').disabled = allUsersEnabledEl.querySelector('input').checked; + }); + + userSelectEl.querySelector('select').disabled = allUsersEnabledEl.querySelector('input').checked; + + // The user select is too small by default + userSelectEl.querySelector('select').style.minWidth = '200px'; + userSelectEl.querySelector('select').style.minHeight = '125px'; + + let allProjectsEnabledEl = XNAT.ui.panel.input.checkbox({ + name: 'allProjectsEnabled', + id: 'allProjectsEnabled', + label: 'All Projects', + description: 'Enable this for all projects', + value: allProjectsEnabled, + }); + + let projectOptions = projects.map((project) => { + return { + value: project, + label: project, + selected: enabledProjectIds.includes(project) + } + }); + + let projectSelectEl = XNAT.ui.panel.select.multiple({ + name: 'projectIds', + id: 'projectIds', + label: 'Projects', + description: 'Select the projects which can access this', + options: projectOptions, + }); + + // Disable the project select if all projects are enabled + allProjectsEnabledEl.querySelector('input').addEventListener('change', (event) => { + projectSelectEl.querySelector('select').disabled = allProjectsEnabledEl.querySelector('input').checked; + }); + + projectSelectEl.querySelector('select').disabled = allProjectsEnabledEl.querySelector('input').checked; + + // The project select is too small by default + projectSelectEl.querySelector('select').style.minWidth = '200px'; + projectSelectEl.querySelector('select').style.minHeight = '125px'; + + let hardwareScopesDescription = spawn('div.hardware-scopes', [ + spawn('p', 'Hardware
    Use this section to define which Hardware ' + + 'this can be used with.'), + ]); + + let allHardwareEnabledEl = XNAT.ui.panel.input.checkbox({ + name: 'allHardwareEnabled', + id: 'allHardwareEnabled', + label: 'All Hardware', + description: 'Allow this to be used with any Hardware.', + value: allHardwareEnabled, + }); + + let hardwareConfigsEl = XNAT.ui.panel.select.multiple({ + name: 'hardwareConfigs', + id: 'hardwareConfigs', + label: 'Hardware Configs', + description: 'Select the Hardware that can be used with this', + options: hardwareConfigs.map((hardwareConfig) => { + return { + value: hardwareConfig.id, + label: hardwareConfig.hardware?.name, + selected: enabledHardwareConfigs.map((enabledHardwareConfig) => enabledHardwareConfig.id).includes(hardwareConfig.id), + } + }), + }); + + // Disable the hardware configs select if all hardware is enabled + allHardwareEnabledEl.querySelector('input').addEventListener('change', (event) => { + hardwareConfigsEl.querySelector('select').disabled = allHardwareEnabledEl.querySelector('input').checked; + }); + + hardwareConfigsEl.querySelector('select').disabled = allHardwareEnabledEl.querySelector('input').checked; + + // The hardware configs select is too small by default + hardwareConfigsEl.querySelector('select').style.minWidth = '200px'; + hardwareConfigsEl.querySelector('select').style.minHeight = '100px'; + + formEl.appendChild(spawn('!', [ + idEl, + typeEl, + nameEl, + imageEl, + spawn('hr'), + environmentVariablesHeaderEl, + addEnvironmentVariableButtonEl, + spawn('hr'), + mountsEl, + addMountButton, + spawn('hr'), + hardwareScopesDescription, + allHardwareEnabledEl, + hardwareConfigsEl, + spawn('hr'), + userAndProjectScopesDescription, + siteEnabledEl, + allProjectsEnabledEl, + projectSelectEl, + allUsersEnabledEl, + userSelectEl, + ])); + + // Add the environment variables + environmentVariables.forEach((environmentVariable) => { + addEnvironmentVariable(environmentVariable); + }); + + // Add the mounts + mounts.forEach((mount) => { + addMount(mount); + }); + }, + buttons: [ + { + label: 'Cancel', + isDefault: false, + close: true + }, + { + label: 'Save', + isDefault: true, + close: false, + action: function() { + const formEl = document.getElementById('compute-spec-editor'); + + const idEl = formEl.querySelector('#id'); + const typeEl = formEl.querySelector('#type'); + const nameEl = formEl.querySelector('#name'); + const imageEl = formEl.querySelector('#image'); + const siteEnabledEl = formEl.querySelector('#siteEnabled'); + const allUsersEnabledEl = formEl.querySelector('#allUsersEnabled'); + const userIdsEl = formEl.querySelector('#userIds'); + const allProjectsEnabledEl = formEl.querySelector('#allProjectsEnabled'); + const projectIdsEl = formEl.querySelector('#projectIds'); + const allHardwareEnabledEl = formEl.querySelector('#allHardwareEnabled'); + const hardwareConfigsEl = formEl.querySelector('#hardwareConfigs'); + + const validators = []; + + let validateNameEl = XNAT.validate(nameEl).reset().chain(); + validateNameEl.is('notEmpty').failure('Name is required'); + validators.push(validateNameEl); + + let validateImageEl = XNAT.validate(imageEl).reset().chain(); + validateImageEl.is('notEmpty').failure('Image is required'); + validators.push(validateImageEl); + // TODO: Validate image format + + let envVarsPresent = document.querySelectorAll('input.key').length > 0; + if (envVarsPresent) { + let validateEnvironmentVariableKeys = XNAT.validate('input.key').chain(); + validateEnvironmentVariableKeys.is('notEmpty').failure('Keys are required') + .is('regex', /^[a-zA-Z_][a-zA-Z0-9_]*$/).failure('Keys must be valid environment variable names'); + validators.push(validateEnvironmentVariableKeys); + } + + let mountsPresent = document.querySelectorAll('.mount-group').length > 0; + if (mountsPresent) { + let validateLocalPaths = XNAT.validate('.mount-group .local-path').chain(); + validateLocalPaths.is('notEmpty').failure('Local paths are required') + .is('uri').failure('Local paths must be a valid URI'); + validators.push(validateLocalPaths); + + let validateContainerPaths = XNAT.validate('.mount-group .container-path').chain(); + validateContainerPaths.is('notEmpty').failure('Container paths are required') + .is('uri').failure('Container paths must be a valid URI'); + validators.push(validateContainerPaths); + } + + // validate fields + let errorMessages = []; + + validators.forEach((validator) => { + if (!validator.check()) { + validator.messages.forEach(message => errorMessages.push(message)); + } + }); + + if (errorMessages.length > 0) { + XNAT.dialog.open({ + title: 'Error', + width: 400, + content: '
    • ' + errorMessages.join('
    • ') + '
    ', + }) + return; + } + + config = { + id: idEl.value, + configTypes: [typeEl.value], + computeSpec: { + name: nameEl.value, + image: imageEl.value, + environmentVariables: getEnvironmentVariables(), + mounts: getMounts(), + }, + scopes: { + Site: { + scope: 'Site', + enabled: siteEnabledEl.checked, + }, + Project: { + scope: 'Project', + enabled: allProjectsEnabledEl.checked, + ids: Array.from(projectIdsEl.selectedOptions).map(option => option.value), + }, + User: { + scope: 'User', + enabled: allUsersEnabledEl.checked, + ids: Array.from(userIdsEl.selectedOptions).map(option => option.value), + } + }, + hardwareOptions: { + allowAllHardware: allHardwareEnabledEl.checked, + hardwareConfigs: Array.from(hardwareConfigsEl.selectedOptions).map(option => { + return { + id: option.value, + } + }), + } + } + + XNAT.plugin.jobTemplates.computeSpecConfigs.save(config) + .then(() => { + refreshTable(); + XNAT.dialog.closeAll(); + XNAT.ui.banner.top(2000, 'Save', 'success'); + }) + .catch((err) => { + console.error(err); + XNAT.ui.banner.top(2000, 'Error Saving', 'error'); + }); + } + } + ] + }); + } + + const refreshTable = () => { + XNAT.plugin.jobTemplates.computeSpecConfigs.getAll(computeSpecType) + .then((data) => { + computeSpecConfigs = data; + + if (computeSpecConfigs.length === 0) { + clearContainer() + container.innerHTML = `

    No Jupyter environments found

    ` + return; + } + + table(); + }) + .catch((err) => { + console.error(err); + clearContainer(); + container.innerHTML = `

    Error loading Jupyter environments

    ` + }); + } + + const deleteConfig = (id) => { + XNAT.plugin.jobTemplates.computeSpecConfigs.delete(id) + .then(() => { + XNAT.ui.banner.top(2000, 'Delete successful', 'success'); + refreshTable(); + }) + .catch((err) => { + XNAT.ui.banner.top(4000, 'Error deleting', 'error'); + console.error(err); + }); + } + + const addEnvironmentVariable = (envVar) => { + const formEl = document.getElementById('compute-spec-editor'); + const environmentVariablesEl = formEl.querySelector('div.environment-variables'); + + const keyEl = spawn('input.form-control.key', { + id: 'key', + placeholder: 'Key', + type: 'text', + value: envVar ? envVar.key : '', + }); + + const equalsEl = spawn('span.input-group-addon', { + style: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + } + }, ['=']); + + const valueEl = spawn('input.form-control.value', { + id: 'value', + placeholder: 'Value', + type: 'text', + value: envVar ? envVar.value : '', + }); + + const removeButtonEl = spawn('button.btn.btn-danger', { + type: 'button', + title: 'Remove', + onclick: () => { + environmentVariableEl.remove(); + } + }, [ + spawn('i.fa.fa-trash'), + ]); + + const environmentVariableEl = spawn('div.form-group', { + style: { + marginBottom: '5px', + } + }, [ + spawn('div.input-group', { + style: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + columnGap: '10px', + } + }, [ + keyEl, + equalsEl, + valueEl, + spawn('span.input-group-btn', [ + removeButtonEl, + ]), + ]), + ]); + + environmentVariablesEl.appendChild(environmentVariableEl); + } + + const getEnvironmentVariables = () => { + const formEl = document.getElementById('compute-spec-editor'); + const environmentVariablesEl = formEl.querySelector('div.environment-variables'); + + let environmentVariables = []; + + Array.from(environmentVariablesEl.children).forEach((environmentVariableEl) => { + const keyEl = environmentVariableEl.querySelector('#key'); + const valueEl = environmentVariableEl.querySelector('#value'); + + if (keyEl === null || valueEl === null) return; + + environmentVariables.push({ + key: keyEl.value, + value: valueEl.value, + }); + }); + + return environmentVariables; + } + + const addMount = (mount) => { + const formEl = document.getElementById('compute-spec-editor'); + const mountsEl = formEl.querySelector('div.mounts'); + + let removeButton = spawn('a.close', { + html: '', + onclick: () => { + mountGroup.remove(); + } + }) + + let localPath = XNAT.ui.panel.input.text({ + name: 'localPath', + label: 'Local Path', + classes: 'required local-path', + description: 'The local path on the host to mount to the container', + value: mount ? mount.localPath : '', + }); + + let containerPath = XNAT.ui.panel.input.text({ + name: 'containerPath', + label: 'Container Path', + classes: 'required container-path', + description: 'The path to mount the local path to in the container', + value: mount ? mount.containerPath : '', + }); + + let readOnly = XNAT.ui.panel.input.checkbox({ + name: 'readOnly', + label: 'Read Only', + classes: 'required read-only', + description: 'Whether or not the mount should be read only', + value: mount ? mount.readOnly : true, + }); + + let mountGroup = spawn('div.mount-group', { + style: { + border: '1px solid #ccc', + padding: '5px', + margin: '5px', + borderRadius: '10px', + } + }, [ + removeButton, + localPath, + containerPath, + readOnly, + ]); + + mountsEl.appendChild(mountGroup); + } + + const getMounts = () => { + const formEl = document.getElementById('compute-spec-editor'); + const mountsEl = formEl.querySelector('div.mounts'); + + let mounts = []; + + Array.from(mountsEl.children).forEach((mountGroup) => { + const localPathEl = mountGroup.querySelector('.local-path'); + const containerPathEl = mountGroup.querySelector('.container-path'); + const readOnlyEl = mountGroup.querySelector('.read-only'); + + if (localPathEl === null || containerPathEl === null || readOnlyEl === null) return; + + mounts.push({ + volumeName: '', + localPath: localPathEl.value, + containerPath: containerPathEl.value, + readOnly: readOnlyEl.checked, + }); + }); + + return mounts; + } + + const table = () => { + const computeSpecTable = XNAT.table.dataTable(computeSpecConfigs, { + header: true, + sortable: 'name', + columns: { + name: { + label: 'Name', + filter: true, + td: { className: 'word-wrapped align-top' }, + apply: function () { + return this['computeSpec']['name']; + } + }, + image: { + label: 'Image', + filter: true, + apply: function () { + return this['computeSpec']['image']; + }, + }, + hardware: { + label: 'Hardware', + td: { className: 'hardware word-wrapped align-top' }, + filter: true, + apply: function () { + return displayHardware(this); + }, + }, + projects: { + label: 'Project(s)', + filter: true, + td: { className: 'projects word-wrapped align-top' }, + apply: function () { + return displayProjects(this); + }, + }, + users: { + label: 'User(s)', + filter: true, + td: { className: 'users word-wrapped align-top' }, + apply: function () { + return displayUsers(this); + }, + }, + enabled: { + label: 'Enabled', + apply: function () { + return spawn('div.center', [enabledToggle(this)]); + }, + }, + actions: { + label: 'Actions', + th: { style: { width: '150px' } }, + apply: function () { + return spawn('div.center', [ + spawn('button.btn.btn-sm', { onclick: () => editor(this, 'edit') }, ''), + spawn('span', { style: { display: 'inline-block', width: '4px' } }), + spawn('button.btn.btn-sm', { onclick: () => editor(this, 'copy') }, ''), + spawn('span', { style: { display: 'inline-block', width: '4px' } }), + spawn('button.btn.btn-sm', { onclick: () => deleteConfig(this.id) }, '') + ]); + }, + } + }, + }) + + clearContainer(); + computeSpecTable.render(`#${containerId}`); + } + + init(); + + return { + container: container, + computeSpecConfigs: computeSpecConfigs, + refresh: refreshTable + }; + } + +})); diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/constraint-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/constraint-configs.js new file mode 100644 index 0000000..a39f691 --- /dev/null +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/constraint-configs.js @@ -0,0 +1,612 @@ +console.debug('Loading constraint-configs.js') + +var XNAT = getObject(XNAT || {}); +XNAT.app = getObject(XNAT.app || {}); +XNAT.plugin = getObject(XNAT.plugin || {}); +XNAT.plugin.jobTemplates = getObject(XNAT.plugin.jobTemplates|| {}); +XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates.constraintConfigs || {}); + +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + return factory(); + } +}(function () { + + XNAT.plugin.jobTemplates.constraintConfigs.get = async (id) => { + console.debug(`Fetching constraint config ${id}`); + const url = XNAT.url.restUrl(`/xapi/job-templates/constraint-configs/${id}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error(`Failed to fetch constraint config ${id}`); + } + } + + XNAT.plugin.jobTemplates.constraintConfigs.getAll = async () => { + console.debug('Fetching all constraint configs'); + const url = XNAT.url.restUrl('/xapi/job-templates/constraint-configs'); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error('Failed to fetch constraint configs'); + } + } + + XNAT.plugin.jobTemplates.constraintConfigs.create = async (constraintConfig) => { + console.debug('Creating constraint config'); + const url = XNAT.url.restUrl('/xapi/job-templates/constraint-configs'); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(constraintConfig) + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error('Failed to create constraint config'); + } + } + + XNAT.plugin.jobTemplates.constraintConfigs.update = async (constraintConfig) => { + console.debug(`Updating constraint config ${constraintConfig.id}`); + const url = XNAT.url.restUrl(`/xapi/job-templates/constraint-configs/${constraintConfig.id}`); + + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(constraintConfig) + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error(`Failed to update constraint config ${constraintConfig.id}`); + } + } + + XNAT.plugin.jobTemplates.constraintConfigs.delete = async (id) => { + console.debug(`Deleting constraint config ${id}`); + const url = XNAT.url.restUrl(`/xapi/job-templates/constraint-configs/${id}`); + + const response = await fetch(url, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error(`Failed to delete constraint config ${id}`); + } + } + + XNAT.plugin.jobTemplates.constraintConfigs.save = async (constraintConfig) => { + console.debug(`Saving constraint config ${constraintConfig.id}`); + if (constraintConfig.id) { + return XNAT.plugin.jobTemplates.constraintConfigs.update(constraintConfig); + } else { + return XNAT.plugin.jobTemplates.constraintConfigs.create(constraintConfig); + } + } + + XNAT.plugin.jobTemplates.constraintConfigs.manager = async (containerId) => { + console.debug(`Initializing constraint config manager in container ${containerId}`); + + let container, + footer, + constraintConfigs = [], + users = [], + projects = []; + + const init = () => { + container = document.getElementById(containerId); + + if (!container) { + throw new Error(`Cannot find container with id ${containerId}`); + } + + clearContainer(); + renderNewButton(); + loadProjects(); + refreshTable(); + } + + const clearContainer = () => { + container.innerHTML = ''; + } + + const renderNewButton = () => { + footer = container.closest('.panel').querySelector('.panel-footer'); + footer.innerHTML = ''; + + let button = spawn('div', [ + spawn('div.pull-right', [ + spawn('button.btn.btn-sm.submit', { html: 'New Constraint', onclick: () => editor(null, 'new') }) + ]), + spawn('div.clear.clearFix') + ]) + + footer.appendChild(button); + } + + const loadProjects = async () => { + let response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/data/projects`); + + if (response.ok) { + let data = await response.json(); + data = data.ResultSet?.Result || []; + data = data.map(p => p['ID']); + projects = data; + } else { + throw new Error(`Error fetching projects`); + } + } + + const deleteConfig = (id) => { + XNAT.plugin.jobTemplates.constraintConfigs.delete(id).then(() => { + XNAT.ui.banner.top(2000, 'Constraint deleted', 'success'); + refreshTable(); + }).catch(err => { + XNAT.ui.banner.top(4000, 'Error deleting constraint', 'error'); + console.error(err); + }); + } + + const displayProjects = (config) => { + let isAllProjectsEnabled = config['scopes']['Project']['enabled']; + let projects = config['scopes']['Project']['ids']; + + if (isAllProjectsEnabled) { + return 'All Projects'; + } else { + if (projects.length > 4) { + function showProjectModal() { + XNAT.dialog.message.open({ + title: 'Enabled Projects', + content: '
    • ' + projects.join('
    • ') + '
    ', + }); + } + + return spawn('span.show-enabled-projects', { + style: { + 'border-bottom': '1px #ccc dashed', + 'cursor': 'pointer' + }, + data: { 'projects': projects.join(',') }, + title: 'Click to view projects', + onclick: showProjectModal + }, + `${projects.length} Projects Enabled` + ); + } else { + if (projects.length === 0) { + return 'No Projects Enabled'; + } + + return projects.sort().join(', '); + } + } + } + + const enabledToggle = (config) => { + let enabled = config['scopes']['Site']['enabled']; + let ckbox = spawn('input', { + type: 'checkbox', + checked: enabled, + value: enabled ? 'true' : 'false', + data: { checked: enabled }, + onchange: () => { + let enabled = ckbox.checked; + config['scopes']['Site']['enabled'] = enabled; + + XNAT.plugin.jobTemplates.constraintConfigs.update(config).then(() => { + XNAT.ui.banner.top(2000, `Constraint ${enabled ? 'Enabled' : 'Disabled'}`, 'success'); + }).catch((err) => { + console.error(err); + XNAT.ui.banner.top(4000, + `Error ${enabled ? 'Enabling' : 'Disabling'} Constraint`, + 'error' + ); + toggleCheckbox(!enabled); + }); + } + }); + + let toggleCheckbox = (enabled) => { + ckbox.checked = enabled; + ckbox.value = enabled ? 'true' : 'false'; + ckbox.dataset.checked = enabled; + } + + return spawn('label.switchbox', [ckbox, ['span.switchbox-outer', [['span.switchbox-inner']]]]); + } + + const editor = (config, mode) => { + console.debug(`Opening constraint config editor in ${mode} mode`); + + let isNew = mode === 'new', + isEdit = mode === 'edit', + isCopy = mode === 'copy'; + + let title = isNew || isCopy ? 'New Constraint' : 'Edit Constraint'; + + XNAT.dialog.open({ + title: title, + content: spawn('div', { id: 'config-editor' }), + width: 650, + maxBtn: true, + beforeShow: () => { + const form = document.getElementById('config-editor'); + form.classList.add('panel'); + + let id = isNew || isCopy ? '' : config.id; + let attribute = isNew ? '' : config.constraint?.key || ''; + let operator = isNew ? 'IN' : config.constraint?.operator || 'IN'; + let values = isNew ? '' : config.constraint?.values.join(',') || ''; + + let siteEnabled = isNew || isCopy ? true : config.scopes?.Site?.enabled || false; + let allUsersEnabled = isNew ? true : config.scopes?.User?.enabled || false; + let enabledUsers = isNew ? [] : config.scopes?.User?.ids || []; + let allProjectsEnabled = isNew ? true : config.scopes?.Project?.enabled || false; + let enabledProjects = isNew ? [] : config.scopes?.Project?.ids || []; + + let idInput = XNAT.ui.panel.input.text({ + name: 'id', + id: 'id', + label: 'ID *', + description: 'The ID of the constraint', + value: id, + }); + + idInput.style.display = 'none'; // hide the ID field + + let constraintHeader = spawn('div.placement-constraints.swarm', [ + spawn('p', 'Constraint
    Use this section to define a placement ' + + 'constraint applicable to all jobs submitted to this cluster. ' + + 'See Docker Swarm documentation on ' + + 'service placement constraints and the ' + + 'docker service create command ' + + 'for more information about allowed constraints.'), + ]); + + let attributeInput = XNAT.ui.panel.input.text({ + name: `constraint-attribute`, + label: 'Node attribute', + classes: 'required swarm-constraint-attribute', + description: 'Attribute you wish to constrain. E.g., node.labels.mylabel or node.role', + value: attribute, + }); + + let operatorInput = XNAT.ui.panel.input.radioGroup({ + name: `constraint-operator`, // random ID to avoid collisions + label: 'Comparator', + classes: 'required swarm-constraint-operator', + items: { 0: { label: 'Equals', value: 'IN' }, 1: { label: 'Does not equal', value: 'NOT_IN' } }, + value: operator, + }) + + let valuesInput = XNAT.ui.panel.input.list({ + name: `constraint-values`, // random ID to avoid collisions + label: 'Possible Values For Constraint', + classes: 'required swarm-constraint-values', + description: 'Comma-separated list of values on which user can constrain the attribute (or a single value if not user-settable). E.g., "worker" or "spot,demand" (do not add quotes). ', + value: values, + }) + + let projectScopesDescription = spawn('div.user-and-project-scopes', [ + spawn('p', 'Projects
    Use this section to limit the scope of a constraint to ' + + "specific projects (and it's subjects and experiments). "), + ]); + + let siteEnabledInput = XNAT.ui.panel.input.checkbox({ + name: 'siteEnabled', + id: 'siteEnabled', + label: 'Site Enabled', + description: 'Enable this hardware for all users and projects', + value: siteEnabled, + }); + + siteEnabledInput.style.display = 'none'; // hide the site enabled field + + let allUsersEnabledInput = XNAT.ui.panel.input.checkbox({ + name: 'allUsersEnabled', + id: 'allUsersEnabled', + label: 'All Users', + description: 'Enable this hardware for all users', + value: allUsersEnabled, + }); + + allUsersEnabledInput.style.display = 'none'; // hide the all users enabled field + + let userOptions = users.map((user) => { + return { + value: user, + label: user, + selected: enabledUsers.includes(user) + } + }); + + let userSelect = XNAT.ui.panel.select.multiple({ + name: 'enabledUsers', + id: 'enabledUsers', + label: 'Users', + description: 'Select the users to enable this hardware for', + options: userOptions, + }); + + userSelect.style.display = 'none'; // hide the user select + + // Disable the user select if all users are enabled + allUsersEnabledInput.querySelector('input').addEventListener('change', (event) => { + userSelect.querySelector('select').disabled = allUsersEnabledInput.querySelector('input').checked; + }); + + userSelect.querySelector('select').disabled = allUsersEnabledInput.querySelector('input').checked; + + // The user select is too small by default + userSelect.querySelector('select').style.minWidth = '200px'; + userSelect.querySelector('select').style.minHeight = '125px'; + + let allProjectsEnabledInput = XNAT.ui.panel.input.checkbox({ + name: 'allProjectsEnabled', + id: 'allProjectsEnabled', + label: 'All Projects', + description: 'Enable this constraint for all projects', + value: allProjectsEnabled, + }); + + let projectOptions = projects.map((project) => { + return { + value: project, + label: project, + selected: enabledProjects.includes(project) + } + }); + + let projectSelect = XNAT.ui.panel.select.multiple({ + name: 'enabledProjects', + id: 'enabledProjects', + label: 'Projects', + description: 'Select the projects to enable this constraint for', + options: projectOptions, + }); + + // Disable the project select if all projects are enabled + allProjectsEnabledInput.querySelector('input').addEventListener('change', (event) => { + projectSelect.querySelector('select').disabled = allProjectsEnabledInput.querySelector('input').checked; + }); + + projectSelect.querySelector('select').disabled = allProjectsEnabledInput.querySelector('input').checked; + + // The project select is too small by default + projectSelect.querySelector('select').style.minWidth = '200px'; + projectSelect.querySelector('select').style.minHeight = '125px'; + + let formFields = [ + idInput, + constraintHeader, + attributeInput, + operatorInput, + valuesInput, + spawn('hr'), + projectScopesDescription, + siteEnabledInput, + allUsersEnabledInput, + userSelect, + allProjectsEnabledInput, + projectSelect, + ]; + + form.appendChild(spawn('!', formFields)); + }, + buttons: [ + { + label: 'Save', + isDefault: true, + close: false, + action: function () { + const form = document.getElementById('config-editor'); + + let id = form.querySelector('#id'); + let attribute = form.querySelector('.swarm-constraint-attribute'); + let operator = form.querySelector('.swarm-constraint-operator input:checked'); + let values = form.querySelector('.swarm-constraint-values'); + + let siteEnabledElement = form.querySelector('#siteEnabled'); + let allUsersEnabledElement = form.querySelector('#allUsersEnabled'); + let enabledUsersElement = form.querySelector('#enabledUsers'); + let allProjectsEnabledElement = form.querySelector('#allProjectsEnabled'); + let enabledProjectsElement = form.querySelector('#enabledProjects'); + + let validators = [] + + let validateAttribute = XNAT.validate(attribute) + .reset().chain() + .is('notEmpty').failure('Attribute is required'); + validators.push(validateAttribute); + + let validateValues = XNAT.validate(values) + .reset().chain() + .is('notEmpty').failure('Value is required'); + validators.push(validateValues); + + let errors = []; + + validators.forEach((validator) => { + if (!validator.check()) { + validator.messages.forEach(message => errors.push(message)); + } + }); + + if (errors.length > 0) { + XNAT.dialog.open({ + title: 'Error', + width: 400, + content: '
    • ' + errors.join('
    • ') + '
    ', + }) + return; + } + + config = { + id: id.value, + constraint: { + key: attribute.value, + operator: operator.value, + values: values.value.split(',').map((value) => value.trim()), + }, + scopes: { + Site: { + scope: 'Site', + enabled: siteEnabledElement.checked, + }, + Project: { + scope: 'Project', + enabled: allProjectsEnabledElement.checked, + ids: Array.from(enabledProjectsElement.selectedOptions).map(option => option.value), + }, + User: { + scope: 'User', + enabled: allUsersEnabledElement.checked, + ids: Array.from(enabledUsersElement.selectedOptions).map(option => option.value), + }, + } + } + + XNAT.plugin.jobTemplates.constraintConfigs.save(config).then(() => { + XNAT.ui.banner.top(2000, 'Saved constraint config', 'success'); + XNAT.dialog.closeAll(); + refreshTable(); + }).catch(err => { + console.error(err); + XNAT.ui.banner.top(4000, 'Error saving constraint config', 'error'); + }); + } + }, + { + label: 'Cancel', + close: true, + isDefault: false + } + ] + }); + } + + const refreshTable = () => { + XNAT.plugin.jobTemplates.constraintConfigs.getAll().then(data => { + constraintConfigs = data; + + if (constraintConfigs.length === 0) { + clearContainer(); + container.innerHTML = `

    No constraints found

    `; + return; + } + + renderTable(); + }).catch(err => { + console.error(err); + clearContainer(); + container.innerHTML = `

    Error fetching constraints

    `; + }) + } + + const renderTable = () => { + let table = XNAT.table.dataTable(constraintConfigs, { + header: true, + sortable: 'nodeAttribute, comparator, values', + columns: { + nodeAttribute: { + label: 'Node Attribute', + td: { className: 'word-wrapped align-top' }, + apply: function() { + return this.constraint?.key; + } + }, + comparator: { + label: 'Comparator', + td: { className: 'word-wrapped align-top' }, + apply: function() { + return spawn('div.center', [this.constraint?.operator === 'IN' ? '==' : '!=']); + } + }, + values: { + label: 'Constraint Value(s)', + td: { className: 'word-wrapped align-top' }, + apply: function() { + return spawn('div.center', [this.constraint?.values?.join(', ')]); + } + }, + projects: { + label: 'Project(s)', + td: { className: 'projects word-wrapped align-top' }, + apply: function () { + return displayProjects(this); + } + }, + enabled: { + label: 'Enabled', + apply: function () { + return spawn('div.center', [enabledToggle(this)]); + }, + }, + actions: { + label: 'Actions', + th: { style: { width: '150px' } }, + apply: function () { + return spawn('div.center', [ + spawn('button.btn.btn-sm', + { onclick: () => editor(this, 'edit') }, + '' + ), + spawn('span', { style: { display: 'inline-block', width: '4px' } }), + spawn('button.btn.btn-sm', + { onclick: () => editor(this, 'copy') }, + '' + ), + spawn('span', { style: { display: 'inline-block', width: '4px' } }), + spawn('button.btn.btn-sm', + { onclick: () => deleteConfig(this.id) }, + '' + ) + ]); + }, + } + } + }); + + clearContainer(); + table.render(`#${containerId}`); + } + + init(); + + return { + container: container, + constraintConfigs: constraintConfigs, + refreshTable: refreshTable + } + } + +})); \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/hardware-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/hardware-configs.js new file mode 100644 index 0000000..9528f42 --- /dev/null +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/hardware-configs.js @@ -0,0 +1,1088 @@ +console.debug("Loading hardware-configs.js"); + +var XNAT = getObject(XNAT || {}); +XNAT.app = getObject(XNAT.app || {}); +XNAT.plugin = getObject(XNAT.plugin || {}); +XNAT.plugin.jobTemplates = getObject(XNAT.plugin.jobTemplates || {}); +XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.hardwareConfigs || {}); + +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + return factory(); + } +}(function () { + + XNAT.plugin.jobTemplates.hardwareConfigs.get = async (id) => { + console.debug("Fetching hardware config with id: " + id); + const url = XNAT.url.restUrl('/xapi/job-templates/hardware-configs/' + id); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error('Error fetching hardware config with id: ' + id); + } + } + + XNAT.plugin.jobTemplates.hardwareConfigs.getAll = async () => { + console.debug("Fetching all hardware configs"); + const url = XNAT.url.restUrl('/xapi/job-templates/hardware-configs'); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error('Error fetching all hardware configs'); + } + } + + XNAT.plugin.jobTemplates.hardwareConfigs.create = async (config) => { + console.debug("Creating hardware config"); + const url = XNAT.url.restUrl('/xapi/job-templates/hardware-configs'); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(config) + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error('Error creating hardware config'); + } + }; + + XNAT.plugin.jobTemplates.hardwareConfigs.update = async (config) => { + console.debug("Updating hardware config"); + const id = config.id; + + if (!id) { + throw new Error('Hardware config id is required'); + } + + const url = XNAT.url.restUrl(`/xapi/job-templates/hardware-configs/${id}`); + + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(config) + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error('Error updating hardware config'); + } + } + + XNAT.plugin.jobTemplates.hardwareConfigs.save = async (config) => { + console.debug("Saving hardware config"); + const id = config.id; + + if (!id) { + return XNAT.plugin.jobTemplates.hardwareConfigs.create(config); + } else { + return XNAT.plugin.jobTemplates.hardwareConfigs.update(config); + } + } + + XNAT.plugin.jobTemplates.hardwareConfigs.delete = async (id) => { + console.debug("Deleting hardware config with id: " + id); + const url = XNAT.url.restUrl('/xapi/job-templates/hardware-configs/' + id); + + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Error deleting hardware config with id: ' + id); + } + } + + XNAT.plugin.jobTemplates.hardwareConfigs.manager = async (containerId) => { + console.debug("Initializing hardware config manager"); + + let container, + footer, + hardwareConfigs = [], + users = [], + projects = []; + + const init = () => { + container = document.getElementById(containerId); + + if (!container) { + throw new Error('Container element wtih id: ' + containerId + ' not found'); + } + + clearContainer(); + renderNewButton(); + + loadUsers(); + loadProjects(); + + refreshTable(); + } + + const loadUsers = async () => { + let response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users`); + + if (response.ok) { + let data = await response.json(); + users = data.filter(u => u !== 'jupyterhub' && u !== 'guest'); + } else { + throw new Error(`Error fetching users`); + } + } + + const loadProjects = async () => { + let response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/data/projects`); + + if (response.ok) { + let data = await response.json(); + data = data.ResultSet?.Result || []; + data = data.map(p => p['ID']); + projects = data; + } else { + throw new Error(`Error fetching projects`); + } + } + + const clearContainer = () => { + container.innerHTML = ''; + } + + const renderNewButton = () => { + footer = container.closest('.panel').querySelector('.panel-footer'); + footer.innerHTML = ''; + + let button = spawn('div', [ + spawn('div.pull-right', [ + spawn('button.btn.btn-sm.submit', { html: 'New Hardware', onclick: () => editor(null, 'new') }) + ]), + spawn('div.clear.clearFix') + ]) + + footer.appendChild(button); + } + + const enabledToggle = (config) => { + let enabled = config['scopes']['Site']['enabled']; + let ckbox = spawn('input', { + type: 'checkbox', + checked: enabled, + value: enabled ? 'true' : 'false', + data: { checked: enabled }, + onchange: () => { + let enabled = ckbox.checked; + config['scopes']['Site']['enabled'] = enabled; + + XNAT.plugin.jobTemplates.hardwareConfigs.update(config).then(() => { + XNAT.ui.banner.top(2000, `Hardware ${enabled ? 'Enabled' : 'Disabled'}`, 'success'); + }).catch((err) => { + console.error(err); + XNAT.ui.banner.top(4000, + `Error ${enabled ? 'Enabling' : 'Disabling'} Hardware`, + 'error' + ); + toggleCheckbox(!enabled); + }); + } + }); + + let toggleCheckbox = (enabled) => { + ckbox.checked = enabled; + ckbox.value = enabled ? 'true' : 'false'; + ckbox.dataset.checked = enabled; + } + + return spawn('label.switchbox', [ckbox, ['span.switchbox-outer', [['span.switchbox-inner']]]]); + } + + const displayUsers = (config) => { + let isAllUsersEnabled = config['scopes']['User']['enabled']; + let users = config['scopes']['User']['ids']; + + if (isAllUsersEnabled) { + return 'All Users'; + } else { + if (users.length > 4) { + function showUserModal() { + XNAT.dialog.message.open({ + title: 'Enabled Users', + content: '
    • ' + users.join('
    • ') + '
    ', + }); + } + + return spawn('span.show-enabled-users', { + style: { + 'border-bottom': '1px #ccc dashed', + 'cursor': 'pointer' + }, + data: { 'users': users.join(',') }, + title: 'Click to view users', + onclick: showUserModal + }, + `${users.length} Users Enabled` + ); + } else { + if (users.length === 0) { + return 'No Users Enabled'; + } + + return users.sort().join(', '); + } + } + } + + const displayProjects = (config) => { + let isAllProjectsEnabled = config['scopes']['Project']['enabled']; + let projects = config['scopes']['Project']['ids']; + + if (isAllProjectsEnabled) { + return 'All Projects'; + } else { + if (projects.length > 4) { + function showProjectModal() { + XNAT.dialog.message.open({ + title: 'Enabled Projects', + content: '
    • ' + projects.join('
    • ') + '
    ', + }); + } + + return spawn('span.show-enabled-projects', { + style: { + 'border-bottom': '1px #ccc dashed', + 'cursor': 'pointer' + }, + data: { 'projects': projects.join(',') }, + title: 'Click to view projects', + onclick: showProjectModal + }, + `${projects.length} Projects Enabled` + ); + } else { + if (projects.length === 0) { + return 'No Projects Enabled'; + } + + return projects.sort().join(', '); + } + } + } + + const editor = (config, mode) => { + console.debug(`Opening hardware config editor in ${mode} mode`); + + let isNew = mode === 'new', + isEdit = mode === 'edit', + isCopy = mode === 'copy'; + + let title = isNew || isCopy ? 'New Hardware' : 'Edit Hardware'; + + XNAT.dialog.open({ + title: title, + content: spawn('div', { id: 'config-editor' }), + width: 650, + maxBtn: true, + beforeShow: () => { + const form = document.getElementById('config-editor'); + form.classList.add('panel'); + + let id = isNew || isCopy ? '' : config.id; + let name = isNew || isCopy ? '' : config.hardware?.name; + + let cpuLimit = isNew ? '' : config.hardware?.cpuLimit || ''; + let cpuReservation = isNew ? '' : config.hardware?.cpuReservation || ''; + let memoryLimit = isNew ? '' : config.hardware?.memoryLimit || ''; + let memoryReservation = isNew ? '' : config.hardware?.memoryReservation || ''; + + let environmentVariables = isNew ? [] : config.hardware?.environmentVariables || []; + let constraints = isNew ? [] : config.hardware?.constraints || []; + let genericResources = isNew ? [] : config.hardware?.genericResources || []; + + let siteEnabled = isNew || isCopy ? true : config.scopes?.Site?.enabled || false; + let allUsersEnabled = isNew ? true : config.scopes?.User?.enabled || false; + let enabledUsers = isNew ? [] : config.scopes?.User?.ids || []; + let allProjectsEnabled = isNew ? true : config.scopes?.Project?.enabled || false; + let enabledProjects = isNew ? [] : config.scopes?.Project?.ids || []; + + let idInput = XNAT.ui.panel.input.text({ + name: 'id', + id: 'id', + label: 'ID *', + description: 'The ID of the hardware configuration', + value: id, + }); + + idInput.style.display = 'none'; // hide the ID field + + let nameInput = XNAT.ui.panel.input.text({ + name: 'name', + id: 'name', + label: 'Name *', + description: 'Provide a user-friendly name for this hardware', + value: name, + }); + + let cpuAndMemDescriptionSwarm = spawn('div.cpu-mem-header.swarm', [ + spawn('p', 'CPU and Memory
    By default, a container has no resource ' + + 'constraints and can use as much of a given resource as the host’s kernel scheduler allows.' + + ' Docker provides ways to control how much memory, or CPU a container can use, setting ' + + 'runtime configuration flags of the docker run command. ' + + 'For more information, see the Docker documentation on ' + + '' + + 'resource constraints.'), + ]); + + let cpuReservationInput = XNAT.ui.panel.input.text({ + name: 'cpuReservation', + id: 'cpuReservation', + label: 'CPU Reservation', + description: 'The number of CPUs to reserve', + value: cpuReservation, + }); + + let cpuLimitInput = XNAT.ui.panel.input.text({ + name: 'cpuLimit', + id: 'cpuLimit', + label: 'CPU Limit', + description: 'The maximum number of CPUs that can be used', + value: cpuLimit, + }); + + let memoryReservationInput = XNAT.ui.panel.input.text({ + name: 'memoryReservation', + id: 'memoryReservation', + label: 'Memory Reservation', + description: 'The amount of memory that this container will reserve. Allows for suffixes like "0.5G" or "512M".', + value: memoryReservation, + }); + + let memoryLimitInput = XNAT.ui.panel.input.text({ + name: 'memoryLimit', + id: 'memoryLimit', + label: 'Memory Limit', + description: 'The maximum amount of memory that this container can use. Allows for suffixes like "8G" or "4G".', + value: memoryLimit, + }); + + let environmentVariablesHeaderEl = spawn('div.environment-variables', [ + spawn('p', 'Environment Variables
    Use this section to define additional ' + + 'environment variables for the container.'), + ]); + + let addEnvironmentVariableButtonEl = spawn('button.btn.btn-sm.add-environment-variable', { + html: 'Add Environment Variable', + style: { 'margin-top': '0.75em' }, + onclick: () => { + addEnvironmentVariable(); + } + }); + + let genericResourcesDescription = spawn('div.generic-resources.swarm', [ + spawn('p', 'Generic Resources
    Use this section to request generic ' + + 'resources when the container is scheduled. ' + + 'See Docker Swarm documentation on ' + + 'generic resources ' + + 'for more information about allowed constraints.'), + ]); + + let addGenericResourceButton = spawn('button.btn.btn-sm.add-generic-resource.swarm', { + html: 'Add Generic Resource', + style: { 'margin-top': '0.75em' }, + onclick: () => { + addGenericResource(); + } + }); + + let placementConstraintHeaderEl = spawn('div.placement-constraints.swarm', [ + spawn('p', 'Placement Constraints
    Use this section to define placement ' + + 'constraints when the container is scheduled. ' + + 'See Docker Swarm documentation on ' + + 'service placement constraints and the ' + + 'docker service create command ' + + 'for more information about allowed constraints.'), + ]); + + let addSwarmConstraintButtonEl = spawn('button.btn.btn-sm.add-placement-constraint.swarm', { + html: 'Add Constraint', + style: { 'margin-top': '0.75em' }, + onclick: () => { + addSwarmConstraint(); + } + }); + + let userAndProjectScopesDescription = spawn('div.user-and-project-scopes', [ + spawn('p', 'Projects and Users
    Use this section to define which projects ' + + 'and users will have access to this hardware.'), + ]); + + let siteEnabledInput = XNAT.ui.panel.input.checkbox({ + name: 'siteEnabled', + id: 'siteEnabled', + label: 'Site Enabled', + description: 'Enable this hardware for all users and projects', + value: siteEnabled, + }); + + siteEnabledInput.style.display = 'none'; // hide the site enabled field + + let allUsersEnabledInput = XNAT.ui.panel.input.checkbox({ + name: 'allUsersEnabled', + id: 'allUsersEnabled', + label: 'All Users', + description: 'Enable this hardware for all users', + value: allUsersEnabled, + }); + + let userOptions = users.map((user) => { + return { + value: user, + label: user, + selected: enabledUsers.includes(user) + } + }); + + let userSelect = XNAT.ui.panel.select.multiple({ + name: 'enabledUsers', + id: 'enabledUsers', + label: 'Users', + description: 'Select the users to enable this hardware for', + options: userOptions, + }); + + // Disable the user select if all users are enabled + allUsersEnabledInput.querySelector('input').addEventListener('change', (event) => { + userSelect.querySelector('select').disabled = allUsersEnabledInput.querySelector('input').checked; + }); + + userSelect.querySelector('select').disabled = allUsersEnabledInput.querySelector('input').checked; + + // The user select is too small by default + userSelect.querySelector('select').style.minWidth = '200px'; + userSelect.querySelector('select').style.minHeight = '125px'; + + let allProjectsEnabledInput = XNAT.ui.panel.input.checkbox({ + name: 'allProjectsEnabled', + id: 'allProjectsEnabled', + label: 'All Projects', + description: 'Enable this hardware for all projects', + value: allProjectsEnabled, + }); + + let projectOptions = projects.map((project) => { + return { + value: project, + label: project, + selected: enabledProjects.includes(project) + } + }); + + let projectSelect = XNAT.ui.panel.select.multiple({ + name: 'enabledProjects', + id: 'enabledProjects', + label: 'Projects', + description: 'Select the projects to enable this hardware for', + options: projectOptions, + }); + + // Disable the project select if all projects are enabled + allProjectsEnabledInput.querySelector('input').addEventListener('change', (event) => { + projectSelect.querySelector('select').disabled = allProjectsEnabledInput.querySelector('input').checked; + }); + + projectSelect.querySelector('select').disabled = allProjectsEnabledInput.querySelector('input').checked; + + // The project select is too small by default + projectSelect.querySelector('select').style.minWidth = '200px'; + projectSelect.querySelector('select').style.minHeight = '125px'; + + let formFields = [ + idInput, + nameInput, + spawn('hr'), + cpuAndMemDescriptionSwarm, + cpuReservationInput, + cpuLimitInput, + memoryReservationInput, + memoryLimitInput, + spawn('hr'), + environmentVariablesHeaderEl, + addEnvironmentVariableButtonEl, + spawn('hr'), + genericResourcesDescription, + addGenericResourceButton, + spawn('hr'), + placementConstraintHeaderEl, + addSwarmConstraintButtonEl, + spawn('hr'), + userAndProjectScopesDescription, + siteEnabledInput, + allProjectsEnabledInput, + projectSelect, + allUsersEnabledInput, + userSelect, + ]; + + form.appendChild(spawn('!', [ + ...formFields + ])); + + // add the environment variables + environmentVariables.forEach((environmentVariable) => { + addEnvironmentVariable(environmentVariable); + }); + + // add the placement constraints + constraints.forEach((constraint) => { + addSwarmConstraint(constraint); + }); + + // add the generic resources + genericResources.forEach((genericResource) => { + addGenericResource(genericResource); + }); + }, + buttons: [ + { + label: 'Save', + isDefault: true, + close: false, + action: function () { + const form = document.getElementById('config-editor'); + + const idElement = form.querySelector('#id'); + const nameElement = form.querySelector('#name'); + + const cpuReservationElement = form.querySelector('#cpuReservation'); + const cpuLimitElement = form.querySelector('#cpuLimit'); + const memoryReservationElement = form.querySelector('#memoryReservation'); + const memoryLimitElement = form.querySelector('#memoryLimit'); + + const siteEnabledElement = form.querySelector('#siteEnabled'); + const allUsersEnabledElement = form.querySelector('#allUsersEnabled'); + const enabledUsersElement = form.querySelector('#enabledUsers'); + const allProjectsEnabledElement = form.querySelector('#allProjectsEnabled'); + const enabledProjectsElement = form.querySelector('#enabledProjects'); + + const validators = []; + + let validateNameElement = XNAT.validate(nameElement).reset().chain(); + validateNameElement.is('notEmpty').failure('Name is required'); + validators.push(validateNameElement); + + let validateCpuReservationElement = XNAT.validate(cpuReservationElement).reset().chain(); + validateCpuReservationElement.is('allow-empty') + .is('decimal') + .is('greater-than', 0) + .failure('CPU Reservation must be a positive number or empty'); + validators.push(validateCpuReservationElement); + + let validateCpuLimitElement = XNAT.validate(cpuLimitElement).reset().chain(); + validateCpuLimitElement.is('allow-empty') + .is('decimal') + .is('greater-than', 0) + .failure('CPU Limit must be a positive number or empty'); + validators.push(validateCpuLimitElement); + + let validateMemoryReservationElement = XNAT.validate(memoryReservationElement).reset().chain(); + validateMemoryReservationElement.is('allow-empty') + .is('regex', /^$|^([1-9]+[0-9]*)+[KMGT]$/) // 512M, 2G, etc. + .failure('Memory Reservation must be a number followed by a suffix of K, M, G, or T or be empty'); + validators.push(validateMemoryReservationElement); + + let validateMemoryLimitElement = XNAT.validate(memoryLimitElement).reset().chain(); + validateMemoryLimitElement.is('allow-empty') + .is('regex', /^$|^([1-9]+[0-9]*)+[KMGT]$/) // 512M, 2G, etc. + .failure('Memory Limit must be a number followed by a suffix of K, M, G, or T or be empty'); + validators.push(validateMemoryLimitElement); + + let envVarsPresent = document.querySelectorAll('input.key').length > 0; + if (envVarsPresent) { + let validateEnvironmentVariableKeys = XNAT.validate('input.key').chain(); + validateEnvironmentVariableKeys.is('notEmpty') + .is('regex', /^[a-zA-Z_][a-zA-Z0-9_]*$/) // must start with a letter or underscore, and only contain letters, numbers, and underscores + .failure('Keys are required and must be a valid environment variable name'); + validators.push(validateEnvironmentVariableKeys); + } + + let swarmConstraintsPresent = document.querySelectorAll('div.swarm-constraint-group').length > 0; + if (swarmConstraintsPresent) { + let validateSwarmConstraintAttributes = XNAT.validate('input.swarm-constraint-attribute').chain(); + validateSwarmConstraintAttributes.is('notEmpty') + .is('regex', /^[a-zA-Z_][a-zA-Z0-9_.-]*$/) // must start with a letter or underscore, and only contain letters, numbers, underscores, hyphens, and periods + .failure('Attributes are required and must be a valid swarm constraint attribute'); + validators.push(validateSwarmConstraintAttributes); + + let validateSwarmConstraintValues = XNAT.validate('input.swarm-constraint-values').chain(); + validateSwarmConstraintValues.is('notEmpty') + .failure('Constraint values are required'); + validators.push(validateSwarmConstraintValues); + } + + let genericResourcesPresent = document.querySelectorAll('div.resource-group').length > 0; + if (genericResourcesPresent) { + let validateGenericResourceNames = XNAT.validate('input.resource-name').chain(); + validateGenericResourceNames.is('notEmpty').failure('Resource names are required') + .is('regex', /^[a-zA-Z_][a-zA-Z0-9_.-]*$/).failure('Invalid resource name'); + validators.push(validateGenericResourceNames); + + let validateGenericResourceValues = XNAT.validate('input.resource-value').chain(); + validateGenericResourceValues.is('notEmpty').failure('Resource values are required'); + validators.push(validateGenericResourceValues); + } + + // Validate the form + let errorMessages = []; + + validators.forEach((validator) => { + if (!validator.check()) { + validator.messages.forEach(message => errorMessages.push(message)); + } + }); + + if (errorMessages.length > 0) { + XNAT.dialog.open({ + title: 'Error', + width: 400, + content: '
    • ' + errorMessages.join('
    • ') + '
    ', + }) + return; + } + + config = { + id: idElement.value, + hardware: { + name: nameElement.value, + cpuReservation: cpuReservationElement.value, + cpuLimit: cpuLimitElement.value, + memoryReservation: memoryReservationElement.value, + memoryLimit: memoryLimitElement.value, + environmentVariables: getEnvironmentVariables(), + constraints: getSwarmConstraints(), + genericResources: getGenericResources(), + }, + scopes: { + Site: { + scope: 'Site', + enabled: siteEnabledElement.checked, + }, + Project: { + scope: 'Project', + enabled: allProjectsEnabledElement.checked, + ids: Array.from(enabledProjectsElement.selectedOptions).map(option => option.value), + }, + User: { + scope: 'User', + enabled: allUsersEnabledElement.checked, + ids: Array.from(enabledUsersElement.selectedOptions).map(option => option.value), + }, + } + } + + XNAT.plugin.jobTemplates.hardwareConfigs.save(config).then(() => { + XNAT.ui.banner.top(2000, 'Hardware saved', 'success'); + XNAT.dialog.closeAll(); + refreshTable(); + }).catch(err => { + XNAT.ui.banner.top(4000, 'Error Saving Hardware', 'error'); + console.error(err); + }); + } + }, + { + label: 'Cancel', + close: true, + isDefault: false + } + ] + }) + + } + + const refreshTable = () => { + XNAT.plugin.jobTemplates.hardwareConfigs.getAll().then(data => { + hardwareConfigs = data; + + if (hardwareConfigs.length === 0) { + clearContainer(); + container.innerHTML = `

    No Hardware found

    `; + return; + } + + // Sort the hardware configs by name + // TODO Sort by name? Sort by something else? + hardwareConfigs = hardwareConfigs.sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }); + + renderTable(); + }).catch(err => { + console.error(err); + clearContainer(); + container.innerHTML = `

    Error loading Hardware

    `; + }); + } + + const deleteConfig = (id) => { + XNAT.plugin.jobTemplates.hardwareConfigs.delete(id).then(() => { + XNAT.ui.banner.top(2000, 'Hardware config deleted', 'success'); + refreshTable(); + }).catch(err => { + XNAT.ui.banner.top(4000, 'Error deleting hardware config', 'error'); + console.error(err); + }); + } + + const addEnvironmentVariable = (envVar) => { + const formEl = document.getElementById('config-editor'); + const environmentVariablesEl = formEl.querySelector('div.environment-variables'); + + const keyEl = spawn('input.form-control.key', { + id: 'key', + placeholder: 'Key', + type: 'text', + value: envVar ? envVar.key : '', + }); + + const equalsEl = spawn('span.input-group-addon', { + style: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + } + }, ['=']); + + const valueEl = spawn('input.form-control.value', { + id: 'value', + placeholder: 'Value', + type: 'text', + value: envVar ? envVar.value : '', + }); + + const removeButtonEl = spawn('button.btn.btn-danger', { + type: 'button', + title: 'Remove', + onclick: () => { + environmentVariableEl.remove(); + } + }, [ + spawn('i.fa.fa-trash'), + ]); + + const environmentVariableEl = spawn('div.form-group', { + style: { + marginBottom: '5px', + } + }, [ + spawn('div.input-group', { + style: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + columnGap: '10px', + } + }, [ + keyEl, + equalsEl, + valueEl, + spawn('span.input-group-btn', [ + removeButtonEl, + ]), + ]), + ]); + + environmentVariablesEl.appendChild(environmentVariableEl); + } + + const getEnvironmentVariables = () => { + const formEl = document.getElementById('config-editor'); + const environmentVariablesEl = formEl.querySelector('div.environment-variables'); + + let environmentVariables = []; + + Array.from(environmentVariablesEl.children).forEach((environmentVariableEl) => { + const keyEl = environmentVariableEl.querySelector('#key'); + const valueEl = environmentVariableEl.querySelector('#value'); + + if (keyEl === null || valueEl === null) return; + + environmentVariables.push({ + key: keyEl.value, + value: valueEl.value, + }); + }); + + return environmentVariables; + } + + const addSwarmConstraint = (placementConstraint) => { + const formEl = document.getElementById('config-editor'); + const placementConstraintsEl = formEl.querySelector('div.placement-constraints.swarm'); + + let removeButton = spawn('a.close', { + html: '', + onclick: () => { + placementConstraintGroup.remove(); + } + }); + + let attributeInput = XNAT.ui.panel.input.text({ + name: `swarm-constraint-attribute-${Math.floor(Math.random() * 1000000)}`, // random ID to avoid collisions + label: 'Node attribute', + classes: 'required swarm-constraint-attribute', + description: 'Attribute you wish to constrain. E.g., node.labels.mylabel or node.role', + value: placementConstraint ? placementConstraint.key : '', + }); + + let operatorInput = XNAT.ui.panel.input.radioGroup({ + name: `constraint-operator-${Math.floor(Math.random() * 1000000)}`, // random ID to avoid collisions + label: 'Comparator', + classes: 'required swarm-constraint-operator', + items: { 0: { label: 'Equals', value: 'IN' }, 1: { label: 'Does not equal', value: 'NOT_IN' } }, + value: placementConstraint ? placementConstraint.operator : 'IN', + }) + + let valuesInput = XNAT.ui.panel.input.list({ + name: `placement-constraint-values-${Math.floor(Math.random() * 1000000)}`, // random ID to avoid collisions + label: 'Possible Values For Constraint', + classes: 'required swarm-constraint-values', + description: 'Comma-separated list of values on which user can constrain the attribute (or a single value if not user-settable). E.g., "worker" or "spot,demand" (do not add quotes). ', + value: placementConstraint ? placementConstraint.values.join(',') : '', + }) + + let placementConstraintGroup = spawn('div.swarm-constraint-group', { + style: { + border: '1px solid #ccc', + padding: '5px', + margin: '5px', + borderRadius: '10px', + } + }, [ + spawn('div.input-group', [ + removeButton, + attributeInput, + operatorInput, + valuesInput, + ]), + ]); + + placementConstraintsEl.appendChild(placementConstraintGroup); + } + + const getSwarmConstraints = () => { + const formEl = document.getElementById('config-editor'); + const constraintGroups = formEl.querySelectorAll('.swarm-constraint-group'); + + let placementConstraints = []; + + Array.from(constraintGroups).forEach((group) => { + if (group === null) return; + + const attributeEl = group.querySelector('.swarm-constraint-attribute'); + const operatorEl = group.querySelector('.swarm-constraint-operator input:checked'); + const valuesEl = group.querySelector('.swarm-constraint-values'); + + if (attributeEl === null || operatorEl === null || valuesEl === null) return; + + placementConstraints.push({ + key: attributeEl.value, + operator: operatorEl.value, + values: valuesEl.value.split(',').map((value) => value.trim()), + }); + }); + + return placementConstraints; + } + + const addGenericResource = (resource) => { + const formEl = document.getElementById('config-editor'); + const resourcesEl = formEl.querySelector('div.generic-resources'); + + let removeButton = spawn('a.close', { + html: '', + onclick: () => { + resourceGroup.remove(); + } + }); + + let nameInput = XNAT.ui.panel.input.text({ + name: `resource-name-${Math.floor(Math.random() * 1000000)}`, // random ID to avoid collisions + label: 'Name', + classes: 'required resource-name', + description: 'Name of the resource. E.g., GPU', + value: resource ? resource.name : '', + }); + + let valueInput = XNAT.ui.panel.input.text({ + name: `resource-value-${Math.floor(Math.random() * 1000000)}`, // random ID to avoid collisions + label: 'Value', + classes: 'required resource-value', + description: 'Value of the resource. E.g., 1', + value: resource ? resource.value : '', + }); + + let resourceGroup = spawn('div.resource-group', { + style: { + border: '1px solid #ccc', + padding: '5px', + margin: '5px', + borderRadius: '10px', + } + }, [ + spawn('div.input-group', [ + removeButton, + nameInput, + valueInput, + ]), + ]); + + resourcesEl.appendChild(resourceGroup); + } + + const getGenericResources = () => { + const formEl = document.getElementById('config-editor'); + const resourceGroups = formEl.querySelectorAll('.resource-group'); + + let resources = []; + + Array.from(resourceGroups).forEach((group) => { + if (group === null) return; + + const nameEl = group.querySelector('.resource-name'); + const valueEl = group.querySelector('.resource-value'); + + if (nameEl === null || valueEl === null) return; + + resources.push({ + name: nameEl.value, + value: valueEl.value, + }); + }); + + return resources; + } + + const renderTable = () => { + let table = XNAT.table.dataTable(hardwareConfigs, { + header: true, + sortable: 'name', + columns: { + name: { + label: 'Name', + filter: true, + td: { className: 'word-wrapped align-top' }, + apply: function () { + return this.hardware?.name || 'N/A'; + } + }, + cpu: { + label: 'CPU Res / Lim', + filter: true, + apply: function () { + let cpuReservation = this.hardware?.cpuReservation || '-'; + let cpuLimit = this.hardware?.cpuLimit || '-'; + return spawn('div.center', `${cpuReservation} / ${cpuLimit}`); + } + }, + memory: { + label: 'Memory Res / Lim', + filter: true, + apply: function () { + let memoryReservation = this.hardware?.memoryReservation || '-'; + let memoryLimit = this.hardware?.memoryLimit || '-'; + return spawn('div.center', `${memoryReservation} / ${memoryLimit}`); + } + }, + projects: { + label: 'Project(s)', + filter: true, + td: { className: 'projects word-wrapped align-top' }, + apply: function () { + return displayProjects(this); + } + }, + users: { + label: 'User(s)', + filter: true, + td: { className: 'users word-wrapped align-top' }, + apply: function () { + return displayUsers(this); + }, + }, + enabled: { + label: 'Enabled', + apply: function () { + return spawn('div.center', [enabledToggle(this)]); + }, + }, + actions: { + label: 'Actions', + th: { style: { width: '150px' } }, + apply: function () { + return spawn('div.center', [ + spawn('button.btn.btn-sm', + { onclick: () => editor(this, 'edit') }, + '' + ), + spawn('span', { style: { display: 'inline-block', width: '4px' } }), + spawn('button.btn.btn-sm', + { onclick: () => editor(this, 'copy') }, + '' + ), + spawn('span', { style: { display: 'inline-block', width: '4px' } }), + spawn('button.btn.btn-sm', + { onclick: () => deleteConfig(this.id) }, + '' + ) + ]); + }, + } + } + }) + + clearContainer(); + table.render(`#${containerId}`); + } + + init(); + + return { + container: container, + hardwareConfigs: hardwareConfigs, + refreshTable: refreshTable + } + } + +})); \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-profiles.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-profiles.js deleted file mode 100644 index e131169..0000000 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-profiles.js +++ /dev/null @@ -1,645 +0,0 @@ -// noinspection RegExpRedundantEscape - -/*! - * JupyterHub Profiles - */ - -console.debug('jupyterhub-profiles.js'); - -var XNAT = getObject(XNAT || {}); -XNAT.app = getObject(XNAT.app || {}); -XNAT.app.activityTab = getObject(XNAT.app.activityTab || {}); -XNAT.plugin = getObject(XNAT.plugin || {}); -XNAT.plugin.jupyterhub = getObject(XNAT.plugin.jupyterhub || {}); -XNAT.plugin.jupyterhub.profiles = getObject(XNAT.plugin.jupyterhub.profiles || {}); - -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(factory); - } else if (typeof exports === 'object') { - module.exports = factory(); - } else { - return factory(); - } -}(function () { - - XNAT.plugin.jupyterhub.profiles.empty = () => { - return { - "enabled": true, - "id": null, - "name": "", - "description": "", - "projects": [], - "spawner": "dockerspawner.SwarmSpawner", - "task_template": { - "container_spec": { - "env": {}, - "image": "", - "mounts": [] - }, - "placement": { - "constraints": [] - }, - "resources": { - "cpu_limit": null, - "cpu_reservation": null, - "mem_limit": null, - "mem_reservation": null, - "generic_resources": {} - } - } - } - }; - - XNAT.plugin.jupyterhub.profiles.get = async (id) => { - console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.get(${id})`); - - const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles/${id}`); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error('Error fetching profile'); - } - } - - XNAT.plugin.jupyterhub.profiles.getAll = async () => { - console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.getAll()`); - - const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles`); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error('Error fetching profiles'); - } - } - - XNAT.plugin.jupyterhub.profiles.create = async (profile) => { - console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.create`); - - const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles`); - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(profile) - }); - - if (response.ok) { - return response.text(); - } else { - throw new Error('Error creating profile'); - } - } - - XNAT.plugin.jupyterhub.profiles.update = async (profile) => { - console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.update`); - - const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles/${profile.id}`); - - const response = await fetch(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(profile) - }); - - if (!response.ok) { - throw new Error('Error updating profile'); - } - } - - XNAT.plugin.jupyterhub.profiles.delete = async (id) => { - console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.delete(${id})`); - - const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles/${id}`); - - const response = await fetch(url, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (!response.ok) { - throw new Error('Error deleting profile'); - } - } - - XNAT.plugin.jupyterhub.profiles.getForProject = async (projectId) => { - console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.getForProject(${projectId})`); - - const url = XNAT.url.csrfUrl(`/xapi/jupyterhub/profiles/projects/${projectId}`); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error('Error getting profiles for project'); - } - } - - XNAT.plugin.jupyterhub.profiles.manager = async (containerId = 'jupyterhub-profiles') => { - console.debug(`jupyterhub-profiles.js - XNAT.plugin.jupyterhub.profiles.manager`); - - const containerEl = document.getElementById(containerId); - const footerEl = containerEl.parentElement.parentElement.querySelector('.panel-footer'); - - const clearContainer = () => containerEl.innerHTML = ''; - - const editButton = (profile) => { - return spawn('button.btn.sm', { - title: 'Edit', - onclick: function (e) { - e.preventDefault(); - editor(profile); - } - }, [spawn('span.fa.fa-pencil')]); - } - - const deleteButton = (profile) => { - return spawn('button.btn.sm.delete', { - title: 'Delete', - onclick: function (e) { - e.preventDefault(); - xmodal.confirm({ - title: 'Delete Profile', - height: 200, - width: 600, - scroll: false, - content: "" + - "

    Are you sure you want to delete the profile " + profile.name + "?

    " + - "

    This action cannot be undone.

    ", - okAction: function () { - XNAT.plugin.jupyterhub.profiles.delete(profile.id) - .then(() => { - refreshTable(); - XNAT.ui.banner.top(2000, 'Profile deleted', 'success'); - }) - .catch((err) => { - console.error(err); - XNAT.ui.banner.top(2000, 'Error deleting profile', 'error'); - }); - } - }) - } - }, [spawn('i.fa.fa-trash')]); - } - - const enabledToggle = (profile) => { - let enabled = profile['enabled']; - let ckbox = spawn('input', { - type: 'checkbox', - checked: enabled, - value: enabled ? 'true' : 'false', - data: {checked: enabled}, - onchange: () => { - profile['enabled'] = !enabled; - - XNAT.plugin.jupyterhub.profiles.update(profile).then(() => { - XNAT.ui.banner.top(2000, 'Profile ' + (enabled ? 'disabled' : 'enabled'), 'success'); - }).catch((err) => { - console.error(err); - XNAT.ui.banner.top(2000, 'Error updating profile', 'error'); - }); - } - }); - - return spawn('label.switchbox', [ckbox, ['span.switchbox-outer', [['span.switchbox-inner']]]]); - } - - const spacer = (width) => { - return spawn('i.spacer', { - style: { - display: 'inline-block', - width: width + 'px' - } - }) - } - - const refreshTable = async () => { - const profiles = await XNAT.plugin.jupyterhub.profiles.getAll(); - - if (profiles.length === 0) { - clearContainer(); - containerEl.innerHTML = '

    No profiles found. Click the "New Profile" button to create one.

    '; - return; - } - - const profilesTable = XNAT.ui.panel.dataTable({ - name: 'profile-table', - data: profiles, - items: { - id: '~!', - name: { - label: ' Profile Name', - sort: true - }, - image: { - label: 'Docker Image', - apply: function () { - return spawn('div.center', this['task_template']['container_spec']['image']) - } - }, - cpu: { - label: 'CPU Res / Lim', - apply: function () { - let cpuReservation = - this.task_template && - this.task_template.resources && - this.task_template.resources.cpu_reservation || '-'; - let cpuLimit = - this.task_template && - this.task_template.resources && - this.task_template.resources.cpu_limit || '-'; - let cpu = cpuReservation + ' / ' + cpuLimit; - return spawn('div.center', cpu); - } - }, - memory: { - label: 'Memory Res / Lim', - apply: function () { - let memReservation = - this.task_template && - this.task_template.resources && - this.task_template.resources.mem_reservation || '-'; - let memLimit = - this.task_template && - this.task_template.resources && - this.task_template.resources.mem_limit || '-'; - let mem = memReservation + ' / ' + memLimit; - return spawn('div.center', mem); - } - }, - enables: { - label: 'Enabled', - apply: function () { - return spawn('div.center', [enabledToggle(this)]); - } - }, - actions: { - label: 'Actions', - apply: function () { - return spawn('div.center', [ - editButton(this), - spacer(4), - deleteButton(this) - ]); - } - } - }, - }) - - // Clear the container and add the table - clearContainer(); - containerEl.append(profilesTable.element); - const elementWrapper = profilesTable.element.querySelector('.element-wrapper') - if (elementWrapper) { - // Make the table full width, the default is too narrow - elementWrapper.style.width = '100%'; - } - } - - const editor = (profile) => { - const doWhat = profile ? 'Edit' : 'Create'; - const isNew = !profile; - profile = profile || XNAT.plugin.jupyterhub.profiles.empty(); - - const editorDialogId = 'editor-dialog'; - - XNAT.dialog.open({ - title: `${doWhat} JupyterHub Profile`, - content: spawn(`form#${editorDialogId}`), - maxBtn: true, - width: 600, - beforeShow: () => { - const formEl = document.getElementById(editorDialogId); - formEl.classList.add('panel'); - - let profileName = isNew ? '' : profile.name || ''; - let profileDescription = isNew ? '' : profile.description || ''; - let profileImage = isNew ? '' : - profile.task_template && - profile.task_template.container_spec && - profile.task_template.container_spec.image || ''; - let profileCpuReservation = isNew ? '' : - profile.task_template && - profile.task_template.resources && - profile.task_template.resources.cpu_reservation || ''; - let profileCpuLimit = isNew ? '' : - profile.task_template && - profile.task_template.resources && - profile.task_template.resources.cpu_limit || ''; - let profileMemoryReservation = isNew ? '' : - profile.task_template && - profile.task_template.resources && - profile.task_template.resources.mem_reservation || ''; - let profileMemoryLimit = isNew ? '' : - profile.task_template && - profile.task_template.resources && - profile.task_template.resources.mem_limit || ''; - let profilePlacementConstraints = isNew ? '' : - profile.task_template && - profile.task_template.placement && - profile.task_template.placement.constraints ? - profile.task_template.placement.constraints.join(',\r\n') : ''; - let profileGenericResources = isNew ? '' : - profile.task_template && - profile.task_template.resources && - profile.task_template.resources.generic_resources ? - Object.entries(profile.task_template.resources.generic_resources).map(([k, v]) => `${k}=${v}`).join(',\r\n') : ''; - let profileEnv = isNew ? '' : - profile.task_template && - profile.task_template.container_spec && - profile.task_template.container_spec.env ? - Object.entries(profile.task_template.container_spec.env).map(([k, v]) => `${k}=${v}`).join(',\r\n') : ''; - let profileMounts = isNew ? '' : - profile.task_template && - profile.task_template.container_spec && - profile.task_template.container_spec.mounts ? - profile.task_template.container_spec.mounts.map(m => `${m['source']}:${m['target']}:${m['read_only'] ? 'ro' : 'rw'}`).join(',\r\n') : ''; - - formEl.append(spawn('!', [ - XNAT.ui.panel.input.text({ - name: 'name', - id: 'name', - label: 'Name *', - description: 'The name of this profile. This will be displayed to users when they select this configuration.', - value: profileName, - }).element, - XNAT.ui.panel.input.textarea({ - name: 'description', - id: 'description', - label: 'Description *', - description: 'A description of this profile. This will be displayed to users when they select this configuration.', - value: profileDescription, - rows: 5, - }).element, - XNAT.ui.panel.input.text({ - name: 'image', - id: 'image', - label: 'Docker Image *', - description: 'The Docker image to use for this profile. This should be the full image name, including the tag. Example: jupyter/scipy-notebook:hub-version', - value: profileImage, - }).element, - XNAT.ui.panel.input.text({ - name: 'cpu-reservation', - id: 'cpu-reservation', - label: 'CPU Reservation', - description: 'The number of CPUs that this container will reserve. Fractional values (e.g. 0.5) are allowed.', - value: profileCpuReservation, - }).element, - XNAT.ui.panel.input.text({ - name: 'cpu-limit', - id: 'cpu-limit', - label: 'CPU Limit', - description: 'The maximum number of CPUs that this container can use. Fractional values (e.g. 3.5) are allowed.', - value: profileCpuLimit, - }).element, - XNAT.ui.panel.input.text({ - name: 'memory-reservation', - id: 'memory-reservation', - label: 'Memory Reservation', - description: 'The amount of memory that this container will reserve. Allows for suffixes like "2G" or "256M".', - value: profileMemoryReservation, - }).element, - XNAT.ui.panel.input.text({ - name: 'memory-limit', - id: 'memory-limit', - label: 'Memory Limit', - description: 'The maximum amount of memory that this container can use. Allows for suffixes like "4G" or "512M".', - value: profileMemoryLimit, - }).element, - XNAT.ui.panel.input.textarea({ - name: 'placement-constraints', - id: 'placement-constraints', - label: 'Placement Constraints', - description: 'Enter a list of placement constraints in the form of comma-separated strings. For example: node.role==worker, node.labels.type==gpu', - value: profilePlacementConstraints, - rows: 5, - }).element, - XNAT.ui.panel.input.textarea({ - name: 'generic-resources', - id: 'generic-resources', - label: 'Generic Resources', - description: 'Enter a list of generic resources in the form of comma-separated strings. For example: GPU=2, FPGA=1', - value: profileGenericResources, - rows: 5, - }).element, - XNAT.ui.panel.input.textarea({ - name: 'environment-variables', - id: 'environment-variables', - label: 'Environment Variables', - description: 'Enter a list of environment variables in the form of comma-separated strings. For example: MY_ENV_VAR=foo, ANOTHER_ENV_VAR=bar', - // join with newlines and carriage returns to make it easier to edit - value: profileEnv, - rows: 5, - }).element, - XNAT.ui.panel.input.textarea({ - name: 'mounts', - id: 'mounts', - label: 'Bind Mounts', - description: 'Enter a list of additional bind mounts in the form of comma-separated strings. For example: /tools:/tools:ro, /home:/home:rw', - // join with newlines and carriage returns to make it easier to edit - value: profileMounts, - rows: 5, - }).element, - ])) - }, - buttons: [ - { - label: 'Save', - isDefault: true, - close: false, - action: function () { - const formEl = document.getElementById(editorDialogId); - - const nameEl = formEl.querySelector('#name'); - const descriptionEl = formEl.querySelector('#description'); - const imageEl = formEl.querySelector('#image'); - const cpuLimitEl = formEl.querySelector('#cpu-limit'); - const cpuReservationEl = formEl.querySelector('#cpu-reservation'); - const memoryLimitEl = formEl.querySelector('#memory-limit'); - const memoryReservationEl = formEl.querySelector('#memory-reservation'); - const placementConstraintsEl = formEl.querySelector('#placement-constraints'); - const genericResourcesEl = formEl.querySelector('#generic-resources'); - const environmentVariablesEl = formEl.querySelector('#environment-variables'); - const mountsEl = formEl.querySelector('#mounts'); - - let validateNameEl = XNAT.validate(nameEl).reset().chain(); - validateNameEl.is('notEmpty').failure('Name is required'); - - let validateDescriptionEl = XNAT.validate(descriptionEl).reset().chain(); - validateDescriptionEl.is('notEmpty').failure('Description is required'); - - let validateImageEl = XNAT.validate(imageEl).reset().chain(); - validateImageEl.is('notEmpty').failure('Image is required'); - // must be in the format of image:tag - validateImageEl.is('regex', /^.+:.+$/).failure('Image must be in the format of image:tag'); - - let validateCpuLimitEl = XNAT.validate(cpuLimitEl).reset().chain(); - validateCpuLimitEl.is('allow-empty') - .is('decimal') - .is('greater-than', 0) - .failure('CPU Limit must be a number greater than 0 or be empty'); - - let validateCpuReservationEl = XNAT.validate(cpuReservationEl).reset().chain(); - validateCpuReservationEl.is('allow-empty') - .is('decimal') - .is('greater-than', 0) - .failure('CPU Reservation must be a number greater than 0 or be empty'); - - let validateMemoryLimitEl = XNAT.validate(memoryLimitEl).reset().chain(); - validateMemoryLimitEl.is('allow-empty') - .is('regex', /^$|^([1-9]+[0-9]*)+[KMGT]$/) // 512M, 2G, etc. - .failure('Memory Limit must be a number followed by a suffix of K, M, G, or T or be empty'); - - let validateMemoryReservationEl = XNAT.validate(memoryReservationEl).reset().chain(); - validateMemoryReservationEl.is('allow-empty') - .is('regex', /^$|^([1-9]+[0-9]*)+[KMGT]$/) // 512M, 2G, etc. - .failure('Memory Reservation must be a number followed by a suffix of K, M, G, or T or be empty'); - - let validatePlacementConstraintsEl = XNAT.validate(placementConstraintsEl).reset().chain(); - validatePlacementConstraintsEl.is('allow-empty') - .is('regex', /^$|^([A-Za-z0-9_\.]+(==|!=)[A-Za-z0-9_\.]+,?\s*)+$/) // node.role == worker, node.labels.type == gpu - .failure('Placement Constraints must be in the form of KEY==VALUE, KEY!=VALUE or be empty'); - - let validateGenericResourcesEl = XNAT.validate(genericResourcesEl).reset().chain(); - validateGenericResourcesEl.is('allow-empty') - .is('regex', /^$|^([A-Za-z_][A-Za-z0-9_]*=[^,]+,?\s*)+$/) - .failure('Generic Resources must be in the form of KEY=VALUE or be empty'); - - let validateEnvironmentVariablesEl = XNAT.validate(environmentVariablesEl).reset().chain(); - validateEnvironmentVariablesEl.is('allow-empty') - .is('regex', /^$|^([A-Za-z_][A-Za-z0-9_]*=[^,]+,?\s*)+$/) // MY_ENV_VAR=foo, ANOTHER_ENV_VAR=bar - .failure('Environment Variables must be in the form of KEY=VALUE or be empty'); - - let validateMountsEl = XNAT.validate(mountsEl).reset().chain(); - validateMountsEl.is('allow-empty') - .is('regex', /^$|^\/([A-Za-z0-9_\/-]+:\/[A-Za-z0-9_\/-]+:(ro|rw),?\s*)+$/) // /tools:/tools:ro, /home:/home:rw - .failure('Mounts must be in the form of /source:/target:(ro|rw) or be empty'); - - // validate fields - let errorMessages = []; - let validators = [validateNameEl, validateImageEl, validateDescriptionEl, validateCpuLimitEl, - validateCpuReservationEl, validateMemoryLimitEl, validateMemoryReservationEl, - validatePlacementConstraintsEl, validateGenericResourcesEl, - validateEnvironmentVariablesEl, validateMountsEl]; - - validators.forEach(validator => { - if (!validator.check()) { - validator.messages.forEach(message => errorMessages.push(message)); - } - }) - - if (errorMessages.length) { - // errors? - XNAT.dialog.open({ - title: 'Validation Error', - width: 500, - content: '
    • ' + errorMessages.join('
    • ') + '
    ', - }) - return; - } - - profile['name'] = nameEl.value; - profile['description'] = descriptionEl.value; - profile['task_template']['container_spec']['image'] = imageEl.value; - - profile['task_template']['resources']['cpu_limit'] = cpuLimitEl.value; - profile['task_template']['resources']['cpu_reservation'] = cpuReservationEl.value; - profile['task_template']['resources']['mem_limit'] = memoryLimitEl.value; - profile['task_template']['resources']['mem_reservation'] = memoryReservationEl.value; - profile['task_template']['placement']['constraints'] = placementConstraintsEl.value ? placementConstraintsEl.value.split(',').map(s => s.trim()).filter(s => s) : []; - profile['task_template']['resources']['generic_resources'] = genericResourcesEl.value ? Object.fromEntries(new Map(genericResourcesEl.value.split(',').map(s => s.trim().split('=')))) : {}; - profile['task_template']['container_spec']['env'] = environmentVariablesEl.value ? Object.fromEntries(new Map(environmentVariablesEl.value.split(',').map(s => s.trim().split('=')))) : {}; - - if (mountsEl.value) { - profile['task_template']['container_spec']['mounts'] = mountsEl.value.split(',').map(s => { - const [source, target, readonly] = s.trim().split(':'); - return { - 'source': source, - 'target': target, - 'read_only': readonly === 'ro', - 'type': 'bind' - } - }); - } else { - profile['task_template']['container_spec']['mounts'] = []; - } - - if (isNew) { - XNAT.plugin.jupyterhub.profiles.create(profile) - .then(() => { - refreshTable(); - XNAT.dialog.closeAll(); - XNAT.ui.banner.top(2000, 'Jupyter profile created', 'success'); - }) - .catch((err) => { - console.error(err); - XNAT.ui.banner.top(2000, 'Error creating profile', 'error'); - }); - } else { - XNAT.plugin.jupyterhub.profiles.update(profile) - .then(() => { - refreshTable(); - XNAT.dialog.closeAll(); - XNAT.ui.banner.top(2000, 'Jupyter profile updated', 'success'); - }) - .catch((err) => { - console.error(err); - XNAT.ui.banner.top(2000, 'Error updating profile', 'error'); - }); - } - } - }, - { - label: 'Cancel', - close: true - } - ] - }); - } - - const newProfileButton = spawn('button.new-profile.btn.btn-sm.submit', { - html: 'New Profile', - onclick: () => { - editor(); - } - }) - - footerEl.append(spawn('div.pull-right', [newProfileButton])); - footerEl.append(spawn('div.clear.clearfix')); - - return refreshTable().then(() => { - return { - refreshTable: refreshTable - } - }); - } - -})); \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js index c2893e1..f33feb8 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js @@ -26,18 +26,13 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s let restUrl = XNAT.url.restUrl; - let newServerUrl = XNAT.plugin.jupyterhub.servers.newServerUrl = function(username, servername, xsiType, itemId, - itemLabel, projectId, eventTrackingId, - profileId) { + let newServerUrl = XNAT.plugin.jupyterhub.servers.newServerUrl = function (username, servername) { let url = `/xapi/jupyterhub/users/${username}/server`; - // if (servername !== '') { + // if (servername && servername !== "") { // url = `${url}/${servername}`; // } - - url = `${url}?xsiType=${xsiType}&itemId=${itemId}&itemLabel=${itemLabel}&eventTrackingId=${eventTrackingId}&profileId=${profileId}`; - if (projectId) url = `${url}&projectId=${projectId}`; - + return restUrl(url); } @@ -181,10 +176,7 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s console.debug(`jupyterhub-servers.js: XNAT.plugin.jupyterhub.servers.startServer`); console.debug(`Launching jupyter server. User: ${username}, Server Name: ${servername}, XSI Type: ${xsiType}, ID: ${itemId}, Label: ${itemLabel}, Project ID: ${projectId}, eventTrackingId: ${eventTrackingId}`); - XNAT.plugin.jupyterhub.profiles.getAll().then(profiles => { - // filter out disabled configurations - profiles = profiles.filter(c => c['enabled'] === true); - + XNAT.plugin.jobTemplates.computeSpecConfigs.available("JUPYTERHUB", username, projectId).then(computeSpecConfigs => { const cancelButton = { label: 'Cancel', isDefault: false, @@ -194,16 +186,62 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s const startButton = { label: 'Start Jupyter', isDefault: true, - close: true, + close: false, action: function(obj) { - const profile = obj.$modal.find('#profile').val(); - const profileId = profiles.filter(c => c['name'] === profile)[0]['id']; + const computeSpecConfigId = document.querySelector('select#compute-spec-config').value; + const hardwareConfigId = document.querySelector('select#hardware-config').value; + + if (!computeSpecConfigId) { + XNAT.dialog.open({ + width: 450, + title: "Error", + content: "Please select a Jupyter environment.", + buttons: [ + { + label: 'OK', + isDefault: true, + close: true, + } + ] + }); + + return; + } + + if (!hardwareConfigId) { + XNAT.dialog.open({ + width: 450, + title: "Error", + content: "Please select a hardware configuration.", + buttons: [ + { + label: 'OK', + isDefault: true, + close: true, + } + ] + }); + + return; + } + + const serverStartRequest = { + 'username': username, + 'servername': '', // Not supported yet + 'xsiType': xsiType, + 'itemId': itemId, + 'itemLabel': itemLabel, + 'projectId': projectId, + 'eventTrackingId': eventTrackingId, + 'computeSpecConfigId': computeSpecConfigId, + 'hardwareConfigId': hardwareConfigId, + } XNAT.xhr.ajax({ - url: newServerUrl(username, servername, xsiType, itemId, itemLabel, - projectId, eventTrackingId, profileId), + url: newServerUrl(username, servername), method: 'POST', contentType: 'application/json', + data: JSON.stringify(serverStartRequest), beforeSend: function () { XNAT.app.activityTab.start( 'Start Jupyter Notebook Server' + @@ -215,95 +253,115 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s console.error(`Failed to send Jupyter server request: ${error}`) } }); + + xmodal.closeAll(); + XNAT.ui.dialog.closeAll(); } } - const buttons = (profiles.length === 0) ? [cancelButton] : [cancelButton, startButton]; + const buttons = (computeSpecConfigs.length === 0) ? [cancelButton] : [cancelButton, startButton]; XNAT.dialog.open({ - title: 'Select Jupyter Profile', - content: spawn('form'), + title: 'Start Jupyter', + content: spawn('form#server-start-request-form'), maxBtn: true, - width: 750, + width: 500, beforeShow: function(obj) { - if (profiles.length === 0) { - obj.$modal.find('.xnat-dialog-content').html('
    No Jupyter profiles are configured or enabled. Please contact your XNAT administrator.
    '); + const form = document.getElementById('server-start-request-form'); + form.classList.add('panel'); + + if (computeSpecConfigs.length === 0) { + obj.$modal.find('.xnat-dialog-content').html('
    No compute specs are available for Jupyter. Please contact your administrator.
    '); return; } - let configOptions = profiles.map(c => c['name']) - .map(c => [{value: c}]) - .flat(); + let xnatData = spawn('div.xnat-data', { + style: { + marginTop: '10px', + marginBottom: '40px', + } + }, [ + spawn('h2', 'XNAT Data'), + spawn('p.xnat-data-row.description', 'The following data will be available to your Jupyter Notebook Server'), + spawn('p.xnat-data-row.project', [ + spawn('strong', 'Project '), projectId, + ]), + spawn('p.xnat-data-row.subject', [ + spawn('strong', 'Subject '), itemLabel, + ]), + spawn('p.xnat-data-row.experiment', [ + spawn('strong', 'Experiment '), itemLabel, + ]), + ]); - // spawn new image form - const formContainer$ = obj.$modal.find('.xnat-dialog-content'); - formContainer$.addClass('panel'); + xnatData.querySelectorAll('strong').forEach(s => { + s.style.display = 'inline-block'; + s.style.width = '90px'; + }); - let initialProfile = { - name: profiles[0]['name'], - description: profiles[0]['description'], - image: profiles[0]['task_template']['container_spec']['image'], - cpuLimit: profiles[0]['task_template']['resources']['cpu_limit'] ? profiles[0]['task_template']['resources']['cpu_limit'] : 'No Limit', - cpuReservation: profiles[0]['task_template']['resources']['cpu_reservation'] ? profiles[0]['task_template']['resources']['cpu_reservation'] : 'No Reservation', - get cpu() { - return `${this.cpuReservation} / ${this.cpuLimit}`; - }, - memoryLimit: profiles[0]['task_template']['resources']['mem_limit'] ? profiles[0]['task_template']['resources']['mem_limit'] : 'No Limit', - memoryReservation: profiles[0]['task_template']['resources']['mem_reservation'] ? profiles[0]['task_template']['resources']['mem_reservation'] : 'No Reservation', - get memory() { - return `${this.memoryReservation} / ${this.memoryLimit}`; - } + if (xsiType === 'xnat:experimentData') { + xnatData.querySelector('p.project').remove(); + xnatData.querySelector('p.subject').remove(); + } else if (xsiType === 'xnat:subjectData') { + xnatData.querySelector('p.project').remove(); + xnatData.querySelector('p.experiment').remove(); + } else if (xsiType === 'xnat:projectData') { + xnatData.querySelector('p.subject').remove(); + xnatData.querySelector('p.experiment').remove(); } - - obj.$modal.find('form').append( - spawn('!', [ - XNAT.ui.panel.select.single({ - name: 'profile', - id: 'profile', - options: configOptions, - label: 'Profile', - required: true, - description: "Select the Jupyter profile to use. This will determine the Docker image, computing resources, and other configuration options used for your Jupyter notebook server." - }).element, - XNAT.ui.panel.element({ - label: 'Description', - html: `

    ${initialProfile.description}

    `, - }).element, - XNAT.ui.panel.element({ - label: 'Image', - html: `

    ${initialProfile.image}

    The Docker image that will be used to launch your Jupyter notebook server.
    `, - }).element, - XNAT.ui.panel.element({ - label: 'CPU Reservation / Limit', - html: `

    ${initialProfile.cpu}

    The reservation is the minimum amount of CPU resources that will be guaranteed to your server. The limit is the maximum amount of CPU resources that will be allocated to your server.
    `, - }).element, - XNAT.ui.panel.element({ - label: 'Memory Reservation / Limit', - html: `

    ${initialProfile.memory}

    The reservation is the minimum amount of memory resources that will be guaranteed to your server. The limit is the maximum amount of memory resources that will be allocated to your server.
    `, - }).element, - ]) + + let computeSpecConfigSelect = spawn('div', { style : { marginTop: '20px', marginBottom: '40px', } }, [ + spawn('h2', 'Jupyter Environment'), + spawn('p.description', 'Select from the list of Jupyter environments available to you. This determines the software available to your Jupyter notebook server.'), + spawn('select#compute-spec-config', [ + spawn('option', {value: ''}, 'Select a Jupyter environment'), + ...computeSpecConfigs.map(c => spawn('option', {value: c['id']}, c['computeSpec']['name'])), + ])] + ); + + let hardwareConfigSelect = spawn('div', { style : { marginTop: '20px', marginBottom: '40px', } }, [ + spawn('h2', 'Hardware'), + spawn('p.description', 'Select from the list of available Hardware. This determines the memory, CPU and other hardware resources available to your Jupyter notebook server.'), + spawn('select#hardware-config', [ + spawn('option', {value: ''}, 'Select Hardware'), + ])] ); - let profileSelector = document.getElementById('profile'); - profileSelector.addEventListener('change', () => { - let description = document.querySelector('.profile-description'); - let profile = profiles.filter(c => c['name'] === profileSelector.value)[0]; - description.innerHTML = profile['description']; - - let image = document.querySelector('.profile-image'); - image.innerHTML = profile['task_template']['container_spec']['image']; - - let cpu = document.querySelector('.profile-cpu'); - let cpuLimit = profile['task_template']['resources']['cpu_limit'] ? profile['task_template']['resources']['cpu_limit'] : 'No Limit'; - let cpuReservation = profile['task_template']['resources']['cpu_reservation'] ? profile['task_template']['resources']['cpu_reservation'] : 'No Reservation'; - cpu.innerHTML = `${cpuReservation} / ${cpuLimit}`; - - let memory = document.querySelector('.profile-memory'); - let memoryLimit = profile['task_template']['resources']['mem_limit'] ? profile['task_template']['resources']['mem_limit'] : 'No Limit'; - let memoryReservation = profile['task_template']['resources']['mem_reservation'] ? profile['task_template']['resources']['mem_reservation'] : 'No Reservation'; - memory.innerHTML = `${memoryReservation} / ${memoryLimit}`; + form.appendChild(spawn('!', [ + xnatData, + computeSpecConfigSelect, + hardwareConfigSelect + ])); + + computeSpecConfigSelect.querySelector('select').addEventListener('change', () => { + let hardwareSelect = document.getElementById('hardware-config'); + hardwareSelect.innerHTML = ''; + hardwareSelect.appendChild(spawn('option', {value: ''}, 'Select Hardware')); + let computeSpecConfig = computeSpecConfigs.filter(c => c['id'].toString() === computeSpecConfigSelect.querySelector('select').value)[0]; + let hardwareConfigs = computeSpecConfig['hardwareOptions']['hardwareConfigs']; + hardwareConfigs.forEach(h => { + hardwareSelect.appendChild(spawn('option', {value: h['id']}, h['hardware']['name'])); + }); + }); + + form.style.marginLeft = '30px'; + form.style.marginRight = '30px'; + + form.querySelectorAll('p.description').forEach(p => { + p.style.marginTop = '0'; + p.style.marginBottom = '15px'; + p.style.color = '#777'; + }); + + form.querySelectorAll('h2').forEach(h => { + h.style.marginTop = '0'; + h.style.marginBottom = '5px'; + h.style.color = '#222'; }); + form.querySelectorAll('select').forEach(s => { + s.style.width = '100%'; + }); }, buttons: buttons }); diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js index b1aa032..de43f56 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js @@ -100,7 +100,6 @@ XNAT.plugin.jupyterhub.users.tokens = getObject(XNAT.plugin.jupyterhub.users.tok // add table header row usersTable.tr() .th({addClass: 'left', html: 'User'}) - .th('Admin') .th('Server') .th('Ready') .th('Started') @@ -185,25 +184,39 @@ XNAT.plugin.jupyterhub.users.tokens = getObject(XNAT.plugin.jupyterhub.users.tok } XNAT.plugin.jupyterhub.users.getUsers().then(users => { + let noRunningServers = true; + users.forEach(user => { let name = user['name']; let admin = user['admin'] ? 'admin' : ''; let servers = user['servers']; - let hasServer = '' in servers; + let hasServer = '' in servers; // TODO: handle multiple servers, '' is the default server name let url = hasServer ? servers['']['url'] : ''; let ready = hasServer ? servers['']['ready'] : ''; let started = hasServer ? new Date(servers['']['started']) : ''; let lastActivity = hasServer ? new Date(servers['']['last_activity']) : ''; - usersTable.tr() - .td([spawn('div.left', [name])]) - .td([spawn('div.center', [admin])]) - .td([spawn('div.center', [hasServer ? serverDialog(user['name'], servers['']) : ''])]) - .td([spawn('div.center', [ready])]) - .td([spawn('div.center', [started.toLocaleString()])]) - .td([spawn('div.center', [lastActivity.toLocaleString()])]) - .td([spawn('div.center', [hasServer ? stopServerButton(name, '') : ''])]); + if (hasServer) { + noRunningServers = false; + usersTable.tr() + .td([spawn('div.left', [name])]) + .td([spawn('div.center', [hasServer ? serverDialog(user['name'], servers['']) : ''])]) + .td([spawn('div.center', [ready])]) + .td([spawn('div.center', [started.toLocaleString()])]) + .td([spawn('div.center', [lastActivity.toLocaleString()])]) + .td([spawn('div.center', [hasServer ? stopServerButton(name, '') : ''])]); + } }) + + if (noRunningServers) { + usersTable.tr() + .td([spawn('div.left', ["No users currently running a Jupyter server."])]) + .td([spawn('div.center', [])]) + .td([spawn('div.center', [])]) + .td([spawn('div.center', [])]) + .td([spawn('div.center', [])]) + .td([spawn('div.center', [])]); + } }).catch(e => { console.error("Unable to fetch user activity.", e); @@ -213,7 +226,6 @@ XNAT.plugin.jupyterhub.users.tokens = getObject(XNAT.plugin.jupyterhub.users.tok .td([spawn('div.center', [])]) .td([spawn('div.center', [])]) .td([spawn('div.center', [])]) - .td([spawn('div.center', [])]) .td([spawn('div.center', [])]); }) diff --git a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm index 8e96e2e..41e5795 100644 --- a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm +++ b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm @@ -8,8 +8,9 @@ - - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index 5453316..4848444 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -146,26 +146,75 @@ userAuthorization: content: > XNAT.plugin.jupyterhub.users.authorization.initTable('jupyterhub-user-auth-container') -jupyterHubProfiles: +jupyterEnvironments: kind: panel - name: jupyterHubProfiles - label: JupyterHub Profiles + name: jupyterEnvironments + label: Jupyter Environments contents: description: tag: div.message + element: + style: "margin-bottom: 20px;" + contents: + "You can configure Jupyter environments in which users Jupyter notebook servers will run. These environments + are associated with a specific Docker image and additional configuration options (such as environment variables + and mounts) can be specified. An environment can also be associated with a Hardware configuration that + defines the hardware resources that will be allocated to the single-user server when it is launched." + computeSpecsTable: + tag: "div#jupyterhub-compute-spec-configs-table" + computeSpecsScript: + tag: script|src="~/scripts/xnat/plugin/jobTemplates/compute-spec-configs.js" + hardwareConfigsScript: + tag: script|src="~/scripts/xnat/plugin/jobTemplates/hardware-configs.js" + renderComputeSpecsTable: + tag: script + content: > + XNAT.plugin.jobTemplates.computeSpecConfigs.manager('jupyterhub-compute-spec-configs-table', 'JUPYTERHUB'); + +hardwareConfigs: + kind: panel + name: hardwareConfigs + label: Hardware + contents: + description: + tag: div.message + element: + style: "margin-bottom: 20px;" + contents: + "You can create Hardware configurations that define the hardware resources that will be allocated to the + single-user server when it is launched. Additional configuration options (such as cpu and memory limits) + can be specified." + hardwareConfigsTable: + tag: "div#jupyterhub-hardware-configs-table" + hardwareConfigsScript: + tag: script|src="~/scripts/xnat/plugin/jobTemplates/hardware-configs.js" + renderHardwareConfigsTable: + tag: script + content: > + XNAT.plugin.jobTemplates.hardwareConfigs.manager('jupyterhub-hardware-configs-table'); + +constraints: + kind: panel + name: constraints + label: Constraints + contents: + description: + tag: div.message + element: + style: "margin-bottom: 20px;" contents: - "You can create configurations for multiple user environments and let users select from them when they launch a - Jupyter notebook server. This is done by creating multiple profiles, each of which is attached to a set of - configuration options that will be applied to the single-user server when it is launched. This can be used to - let users choose among Docker images or to select the hardware resources they want to use." - profilesTable: - tag: "div#jupyterhub-profiles-table" - profilesScript: - tag: script|src="~/scripts/xnat/plugin/jupyterhub/jupyterhub-profiles.js" - renderProfilesTable: + "Constraints let you configure containers to run only on nodes with specific (arbitrary) metadata set, and + cause the deployment to fail if appropriate nodes do not exist. For instance, you can specify that your service + should only run on worker nodes." + constraintsTable: + tag: "div#jupyterhub-constraints-table" + constraintsScript: + tag: script|src="~/scripts/xnat/plugin/jobTemplates/constraint-configs.js" + renderConstraintsTable: tag: script content: > - XNAT.plugin.jupyterhub.profiles.manager('jupyterhub-profiles-table'); + console.log('rendering constraints table'); + XNAT.plugin.jobTemplates.constraintConfigs.manager('jupyterhub-constraints-table'); jupyterhubPreferences: kind: panel.form @@ -192,7 +241,7 @@ jupyterHubUserActivity: jupyterHubActivityDescription: tag: div.message contents: - "This panel lists all JupyterHub users and any Jupyter notebook servers they may be running." + "This panel lists any active Jupyter users and the status of their Jupyter notebook servers." jupyterHubActivityTable: tag: "div#jupyterhub-user-activity-table" imagePreferencesScript: @@ -224,7 +273,9 @@ siteSettings: contents: ${jupyterHubSetup} ${userAuthorization} - ${jupyterHubProfiles} + ${jupyterEnvironments} + ${hardwareConfigs} + ${constraints} jupyterhubUserActivityTab: kind: tab name: jupyterhubUserActivityTab diff --git a/src/main/resources/jupyterhub-logback.xml b/src/main/resources/jupyterhub-logback.xml index 1c0265d..5177e33 100644 --- a/src/main/resources/jupyterhub-logback.xml +++ b/src/main/resources/jupyterhub-logback.xml @@ -10,8 +10,21 @@ ${xnat.home}/logs/xnat-jupyterhub-plugin.log.%d{yyyy-MM-dd} + + true + ${xnat.home}/logs/job-templates.log + + %d [%t] %-5p %c - %m%n + + + ${xnat.home}/logs/job-templates.log.%d{yyyy-MM-dd} + + + + + diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfilesApiConfig.java b/src/test/java/org/nrg/jobtemplates/config/ComputeSpecConfigsApiConfig.java similarity index 74% rename from src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfilesApiConfig.java rename to src/test/java/org/nrg/jobtemplates/config/ComputeSpecConfigsApiConfig.java index ad7edf3..af84967 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfilesApiConfig.java +++ b/src/test/java/org/nrg/jobtemplates/config/ComputeSpecConfigsApiConfig.java @@ -1,10 +1,10 @@ -package org.nrg.xnatx.plugins.jupyterhub.config; +package org.nrg.jobtemplates.config; import org.nrg.framework.services.ContextService; +import org.nrg.jobtemplates.rest.ComputeSpecConfigsApi; +import org.nrg.jobtemplates.services.ComputeSpecConfigService; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xnatx.plugins.jupyterhub.rest.JupyterHubProfilesApi; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -19,15 +19,15 @@ @EnableWebMvc @EnableWebSecurity @Import({MockConfig.class, RestApiTestConfig.class}) -public class JupyterHubProfilesApiConfig extends WebSecurityConfigurerAdapter { +public class ComputeSpecConfigsApiConfig extends WebSecurityConfigurerAdapter { @Bean - public JupyterHubProfilesApi jupyterHubProfilesApi(final UserManagementServiceI mockUserManagementService, + public ComputeSpecConfigsApi computeSpecConfigsApi(final UserManagementServiceI mockUserManagementService, final RoleHolder mockRoleHolder, - final ProfileService mockProfileService) { - return new JupyterHubProfilesApi(mockUserManagementService, + final ComputeSpecConfigService mockComputeSpecConfigService) { + return new ComputeSpecConfigsApi(mockUserManagementService, mockRoleHolder, - mockProfileService); + mockComputeSpecConfigService); } @Bean @@ -41,4 +41,5 @@ public ContextService contextService(final ApplicationContext applicationContext protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(new TestingAuthenticationProvider()); } + } diff --git a/src/test/java/org/nrg/jobtemplates/config/ConstraintConfigsApiTestConfig.java b/src/test/java/org/nrg/jobtemplates/config/ConstraintConfigsApiTestConfig.java new file mode 100644 index 0000000..5adaee9 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/ConstraintConfigsApiTestConfig.java @@ -0,0 +1,46 @@ +package org.nrg.jobtemplates.config; + +import org.nrg.framework.services.ContextService; +import org.nrg.jobtemplates.rest.ConstraintConfigsApi; +import org.nrg.jobtemplates.services.ConstraintConfigService; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.TestingAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration +@EnableWebMvc +@EnableWebSecurity +@Import({MockConfig.class, RestApiTestConfig.class}) +public class ConstraintConfigsApiTestConfig extends WebSecurityConfigurerAdapter { + + @Bean + public ConstraintConfigsApi placementConstraintConfigsApi(final UserManagementServiceI mockUserManagementService, + final RoleHolder mockRoleHolder, + final ConstraintConfigService mockConstraintConfigService) { + return new ConstraintConfigsApi( + mockUserManagementService, + mockRoleHolder, + mockConstraintConfigService + ); + } + + @Bean + public ContextService contextService(final ApplicationContext applicationContext) { + final ContextService contextService = new ContextService(); + contextService.setApplicationContext(applicationContext); + return contextService; + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) { + auth.authenticationProvider(new TestingAuthenticationProvider()); + } +} diff --git a/src/test/java/org/nrg/jobtemplates/config/DefaultComputeSpecConfigServiceTestConfig.java b/src/test/java/org/nrg/jobtemplates/config/DefaultComputeSpecConfigServiceTestConfig.java new file mode 100644 index 0000000..a2d339b --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/DefaultComputeSpecConfigServiceTestConfig.java @@ -0,0 +1,23 @@ +package org.nrg.jobtemplates.config; + +import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; +import org.nrg.jobtemplates.services.HardwareConfigEntityService; +import org.nrg.jobtemplates.services.impl.DefaultComputeSpecConfigService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({HibernateEntityServicesConfig.class}) +public class DefaultComputeSpecConfigServiceTestConfig { + + @Bean + public DefaultComputeSpecConfigService defaultComputeSpecConfigService(final ComputeSpecConfigEntityService computeSpecConfigEntityService, + final HardwareConfigEntityService hardwareConfigEntityService) { + return new DefaultComputeSpecConfigService( + computeSpecConfigEntityService, + hardwareConfigEntityService + ); + } + +} diff --git a/src/test/java/org/nrg/jobtemplates/config/DefaultConstraintConfigServiceTestConfig.java b/src/test/java/org/nrg/jobtemplates/config/DefaultConstraintConfigServiceTestConfig.java new file mode 100644 index 0000000..f28f15c --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/DefaultConstraintConfigServiceTestConfig.java @@ -0,0 +1,20 @@ +package org.nrg.jobtemplates.config; + +import org.nrg.jobtemplates.services.ConstraintConfigEntityService; +import org.nrg.jobtemplates.services.impl.DefaultConstraintConfigService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({HibernateEntityServicesConfig.class}) +public class DefaultConstraintConfigServiceTestConfig { + + @Bean + public DefaultConstraintConfigService defaultConstraintConfigService(final ConstraintConfigEntityService constraintConfigEntityService) { + return new DefaultConstraintConfigService( + constraintConfigEntityService + ); + } + +} diff --git a/src/test/java/org/nrg/jobtemplates/config/DefaultHardwareConfigServiceTestConfig.java b/src/test/java/org/nrg/jobtemplates/config/DefaultHardwareConfigServiceTestConfig.java new file mode 100644 index 0000000..3c56586 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/DefaultHardwareConfigServiceTestConfig.java @@ -0,0 +1,23 @@ +package org.nrg.jobtemplates.config; + +import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; +import org.nrg.jobtemplates.services.HardwareConfigEntityService; +import org.nrg.jobtemplates.services.impl.DefaultHardwareConfigService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({HibernateEntityServicesConfig.class}) +public class DefaultHardwareConfigServiceTestConfig { + + @Bean + public DefaultHardwareConfigService defaultHardwareConfigService(final HardwareConfigEntityService hardwareConfigEntityService, + final ComputeSpecConfigEntityService computeSpecConfigEntityService) { + return new DefaultHardwareConfigService( + hardwareConfigEntityService, + computeSpecConfigEntityService + ); + } + +} diff --git a/src/test/java/org/nrg/jobtemplates/config/DefaultJobTemplateServiceTestConfig.java b/src/test/java/org/nrg/jobtemplates/config/DefaultJobTemplateServiceTestConfig.java new file mode 100644 index 0000000..5ac44c0 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/DefaultJobTemplateServiceTestConfig.java @@ -0,0 +1,26 @@ +package org.nrg.jobtemplates.config; + +import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.jobtemplates.services.ConstraintConfigService; +import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.jobtemplates.services.impl.DefaultJobTemplateService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({MockConfig.class}) +public class DefaultJobTemplateServiceTestConfig { + + @Bean + public DefaultJobTemplateService defaultJobTemplateService(final ComputeSpecConfigService mockComputeSpecConfigService, + final HardwareConfigService mockHardwareConfigService, + final ConstraintConfigService mockConstraintConfigService) { + return new DefaultJobTemplateService( + mockComputeSpecConfigService, + mockHardwareConfigService, + mockConstraintConfigService + ); + } + +} diff --git a/src/test/java/org/nrg/jobtemplates/config/HardwareConfigsApiConfig.java b/src/test/java/org/nrg/jobtemplates/config/HardwareConfigsApiConfig.java new file mode 100644 index 0000000..267faed --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/HardwareConfigsApiConfig.java @@ -0,0 +1,47 @@ +package org.nrg.jobtemplates.config; + +import org.nrg.framework.services.ContextService; +import org.nrg.jobtemplates.rest.HardwareConfigsApi; +import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.TestingAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration +@EnableWebMvc +@EnableWebSecurity +@Import({MockConfig.class, RestApiTestConfig.class}) +public class HardwareConfigsApiConfig extends WebSecurityConfigurerAdapter { + + @Bean + public HardwareConfigsApi hardwareConfigsApi(final UserManagementServiceI mockUserManagementService, + final RoleHolder mockRoleHolder, + final HardwareConfigService mockHardwareConfigService) { + return new HardwareConfigsApi( + mockUserManagementService, + mockRoleHolder, + mockHardwareConfigService + ); + } + + @Bean + public ContextService contextService(final ApplicationContext applicationContext) { + final ContextService contextService = new ContextService(); + contextService.setApplicationContext(applicationContext); + return contextService; + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) { + auth.authenticationProvider(new TestingAuthenticationProvider()); + } + +} diff --git a/src/test/java/org/nrg/jobtemplates/config/HibernateComputeSpecConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/jobtemplates/config/HibernateComputeSpecConfigEntityServiceTestConfig.java new file mode 100644 index 0000000..569a9ac --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/HibernateComputeSpecConfigEntityServiceTestConfig.java @@ -0,0 +1,79 @@ +package org.nrg.jobtemplates.config; + +import org.hibernate.SessionFactory; +import org.nrg.jobtemplates.entities.*; +import org.nrg.jobtemplates.repositories.ComputeSpecConfigDao; +import org.nrg.jobtemplates.repositories.ConstraintConfigDao; +import org.nrg.jobtemplates.repositories.HardwareConfigDao; +import org.nrg.jobtemplates.services.impl.HibernateComputeSpecConfigEntityService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.orm.hibernate4.HibernateTransactionManager; +import org.springframework.orm.hibernate4.LocalSessionFactoryBean; +import org.springframework.transaction.support.ResourceTransactionManager; + +import javax.sql.DataSource; +import java.util.Properties; + +@Configuration +@Import({HibernateConfig.class}) +public class HibernateComputeSpecConfigEntityServiceTestConfig { + + @Bean + public HibernateComputeSpecConfigEntityService hibernateComputeSpecConfigEntityServiceTest(@Qualifier("computeSpecConfigDaoImpl") final ComputeSpecConfigDao computeSpecConfigDaoImpl, + @Qualifier("hardwareConfigDaoImpl") final HardwareConfigDao hardwareConfigDaoImpl) { + return new HibernateComputeSpecConfigEntityService( + computeSpecConfigDaoImpl, + hardwareConfigDaoImpl); + } + + @Bean + public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qualifier("hibernateProperties") final Properties properties) { + final LocalSessionFactoryBean bean = new LocalSessionFactoryBean(); + bean.setDataSource(dataSource); + bean.setHibernateProperties(properties); + bean.setAnnotatedClasses( + ConstraintConfigEntity.class, + ConstraintEntity.class, + ConstraintScopeEntity.class, + ComputeSpecConfigEntity.class, + ComputeSpecEntity.class, + ComputeSpecScopeEntity.class, + ComputeSpecHardwareOptionsEntity.class, + HardwareConfigEntity.class, + HardwareEntity.class, + HardwareScopeEntity.class, + HardwareConstraintEntity.class, + EnvironmentVariableEntity.class, + MountEntity.class, + GenericResourceEntity.class + ); + return bean; + } + + @Bean + public ResourceTransactionManager transactionManager(final SessionFactory sessionFactory) throws Exception { + return new HibernateTransactionManager(sessionFactory); + } + + @Bean + public ConstraintConfigDao constraintConfigDao(final SessionFactory sessionFactory) { + return new ConstraintConfigDao(sessionFactory); + } + + @Bean + @Qualifier("hardwareConfigDaoImpl") + public HardwareConfigDao hardwareConfigDaoImpl(final SessionFactory sessionFactory) { + return new HardwareConfigDao(sessionFactory); + } + + @Bean + @Qualifier("computeSpecConfigDaoImpl") + public ComputeSpecConfigDao computeSpecConfigDaoImpl(final SessionFactory sessionFactory, + final @Qualifier("hardwareConfigDaoImpl") HardwareConfigDao hardwareConfigDaoImpl) { + return new ComputeSpecConfigDao(sessionFactory, hardwareConfigDaoImpl); + } + +} diff --git a/src/test/java/org/nrg/jobtemplates/config/HibernateConfig.java b/src/test/java/org/nrg/jobtemplates/config/HibernateConfig.java new file mode 100644 index 0000000..aaaa816 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/HibernateConfig.java @@ -0,0 +1,79 @@ +package org.nrg.jobtemplates.config; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.hibernate.SessionFactory; +import org.nrg.jobtemplates.entities.*; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.hibernate4.HibernateTransactionManager; +import org.springframework.orm.hibernate4.LocalSessionFactoryBean; +import org.springframework.transaction.support.ResourceTransactionManager; + +import javax.sql.DataSource; +import java.io.IOException; +import java.util.Properties; + +@Configuration +public class HibernateConfig { + @Bean + public Properties hibernateProperties() throws IOException { + Properties properties = new Properties(); + + // Use HSQLDialect instead of H2Dialect to work around issue + // with h2 version 1.4.200 in hibernate < 5.4 (or so) + // where the generated statements to drop tables between tests can't be executed + // as they do not cascade. + // See https://hibernate.atlassian.net/browse/HHH-13711 + // Solution from https://github.com/hibernate/hibernate-orm/pull/3093#issuecomment-562752874 + properties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); + properties.put("hibernate.hbm2ddl.auto", "create"); + properties.put("hibernate.cache.use_second_level_cache", false); + properties.put("hibernate.cache.use_query_cache", false); + + PropertiesFactoryBean hibernate = new PropertiesFactoryBean(); + hibernate.setProperties(properties); + hibernate.afterPropertiesSet(); + return hibernate.getObject(); + } + + @Bean + public DataSource dataSource() { + BasicDataSource basicDataSource = new BasicDataSource(); + basicDataSource.setDriverClassName(org.h2.Driver.class.getName()); + basicDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); + basicDataSource.setUsername("sa"); + return basicDataSource; + } + + @Bean + public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qualifier("hibernateProperties") final Properties properties) { + final LocalSessionFactoryBean bean = new LocalSessionFactoryBean(); + bean.setDataSource(dataSource); + bean.setHibernateProperties(properties); + bean.setAnnotatedClasses( + ConstraintConfigEntity.class, + ConstraintEntity.class, + ConstraintScopeEntity.class, + ComputeSpecConfigEntity.class, + ComputeSpecEntity.class, + ComputeSpecScopeEntity.class, + ComputeSpecHardwareOptionsEntity.class, + HardwareConfigEntity.class, + HardwareEntity.class, + HardwareScopeEntity.class, + HardwareConstraintEntity.class, + EnvironmentVariableEntity.class, + MountEntity.class, + GenericResourceEntity.class + ); + return bean; + } + + @Bean + public ResourceTransactionManager transactionManager(final SessionFactory sessionFactory) throws Exception { + return new HibernateTransactionManager(sessionFactory); + } + +} diff --git a/src/test/java/org/nrg/jobtemplates/config/HibernateConstraintConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/jobtemplates/config/HibernateConstraintConfigEntityServiceTestConfig.java new file mode 100644 index 0000000..4c11307 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/HibernateConstraintConfigEntityServiceTestConfig.java @@ -0,0 +1,61 @@ +package org.nrg.jobtemplates.config; + +import org.hibernate.SessionFactory; +import org.nrg.jobtemplates.entities.*; +import org.nrg.jobtemplates.repositories.ConstraintConfigDao; +import org.nrg.jobtemplates.services.impl.HibernateConstraintConfigEntityService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.orm.hibernate4.HibernateTransactionManager; +import org.springframework.orm.hibernate4.LocalSessionFactoryBean; +import org.springframework.transaction.support.ResourceTransactionManager; + +import javax.sql.DataSource; +import java.util.Properties; + +@Configuration +@Import({MockConfig.class, HibernateConfig.class}) +public class HibernateConstraintConfigEntityServiceTestConfig { + + @Bean + public HibernateConstraintConfigEntityService hibernateConstraintConfigEntityServiceTest() { + return new HibernateConstraintConfigEntityService(); + } + + @Bean + public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qualifier("hibernateProperties") final Properties properties) { + final LocalSessionFactoryBean bean = new LocalSessionFactoryBean(); + bean.setDataSource(dataSource); + bean.setHibernateProperties(properties); + bean.setAnnotatedClasses( + ConstraintConfigEntity.class, + ConstraintEntity.class, + ConstraintScopeEntity.class, + ComputeSpecConfigEntity.class, + ComputeSpecEntity.class, + ComputeSpecScopeEntity.class, + ComputeSpecHardwareOptionsEntity.class, + HardwareConfigEntity.class, + HardwareEntity.class, + HardwareScopeEntity.class, + HardwareConstraintEntity.class, + EnvironmentVariableEntity.class, + MountEntity.class, + GenericResourceEntity.class + ); + return bean; + } + + @Bean + public ResourceTransactionManager transactionManager(final SessionFactory sessionFactory) throws Exception { + return new HibernateTransactionManager(sessionFactory); + } + + @Bean + public ConstraintConfigDao constraintConfigDao(final SessionFactory sessionFactory) { + return new ConstraintConfigDao(sessionFactory); + } + +} diff --git a/src/test/java/org/nrg/jobtemplates/config/HibernateEntityServicesConfig.java b/src/test/java/org/nrg/jobtemplates/config/HibernateEntityServicesConfig.java new file mode 100644 index 0000000..f251ec7 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/HibernateEntityServicesConfig.java @@ -0,0 +1,60 @@ +package org.nrg.jobtemplates.config; + +import org.hibernate.SessionFactory; +import org.nrg.jobtemplates.repositories.ComputeSpecConfigDao; +import org.nrg.jobtemplates.repositories.ConstraintConfigDao; +import org.nrg.jobtemplates.repositories.HardwareConfigDao; +import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; +import org.nrg.jobtemplates.services.ConstraintConfigEntityService; +import org.nrg.jobtemplates.services.HardwareConfigEntityService; +import org.nrg.jobtemplates.services.impl.HibernateComputeSpecConfigEntityService; +import org.nrg.jobtemplates.services.impl.HibernateConstraintConfigEntityService; +import org.nrg.jobtemplates.services.impl.HibernateHardwareConfigEntityService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({HibernateConfig.class}) +public class HibernateEntityServicesConfig { + + @Bean + public ConstraintConfigDao constraintConfigDao(final SessionFactory sessionFactory) { + return new ConstraintConfigDao(sessionFactory); + } + + @Bean + public ConstraintConfigEntityService constraintConfigEntityService(final ConstraintConfigDao constraintConfigDao) { + HibernateConstraintConfigEntityService service = new HibernateConstraintConfigEntityService(); + service.setDao(constraintConfigDao); + return service; + } + + @Bean + public HardwareConfigDao hardwareConfigDao(final SessionFactory sessionFactory) { + return new HardwareConfigDao(sessionFactory); + } + + @Bean + public ComputeSpecConfigDao computeSpecConfigDao(final SessionFactory sessionFactory, + final HardwareConfigDao hardwareConfigDao) { + return new ComputeSpecConfigDao(sessionFactory, hardwareConfigDao); + } + + @Bean + public ComputeSpecConfigEntityService computeSpecConfigEntityService(final ComputeSpecConfigDao computeSpecConfigDao, + final HardwareConfigDao hardwareConfigDao) { + return new HibernateComputeSpecConfigEntityService( + computeSpecConfigDao, + hardwareConfigDao + ); + } + + @Bean + public HardwareConfigEntityService hardwareConfigEntityService(final HardwareConfigDao hardwareConfigDao) { + HibernateHardwareConfigEntityService service = new HibernateHardwareConfigEntityService(); + service.setDao(hardwareConfigDao); + return service; + } + +} diff --git a/src/test/java/org/nrg/jobtemplates/config/HibernateHardwareConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/jobtemplates/config/HibernateHardwareConfigEntityServiceTestConfig.java new file mode 100644 index 0000000..67b2541 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/HibernateHardwareConfigEntityServiceTestConfig.java @@ -0,0 +1,61 @@ +package org.nrg.jobtemplates.config; + +import org.hibernate.SessionFactory; +import org.nrg.jobtemplates.entities.*; +import org.nrg.jobtemplates.repositories.ConstraintConfigDao; +import org.nrg.jobtemplates.services.impl.HibernateHardwareConfigEntityService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.orm.hibernate4.HibernateTransactionManager; +import org.springframework.orm.hibernate4.LocalSessionFactoryBean; +import org.springframework.transaction.support.ResourceTransactionManager; + +import javax.sql.DataSource; +import java.util.Properties; + +@Configuration +@Import({MockConfig.class, HibernateConfig.class}) +public class HibernateHardwareConfigEntityServiceTestConfig { + + @Bean + public HibernateHardwareConfigEntityService hibernateHardwareConfigEntityServiceTest() { + return new HibernateHardwareConfigEntityService(); + } + + @Bean + public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qualifier("hibernateProperties") final Properties properties) { + final LocalSessionFactoryBean bean = new LocalSessionFactoryBean(); + bean.setDataSource(dataSource); + bean.setHibernateProperties(properties); + bean.setAnnotatedClasses( + ConstraintConfigEntity.class, + ConstraintEntity.class, + ConstraintScopeEntity.class, + ComputeSpecConfigEntity.class, + ComputeSpecEntity.class, + ComputeSpecScopeEntity.class, + ComputeSpecHardwareOptionsEntity.class, + HardwareConfigEntity.class, + HardwareEntity.class, + HardwareScopeEntity.class, + HardwareConstraintEntity.class, + EnvironmentVariableEntity.class, + MountEntity.class, + GenericResourceEntity.class + ); + return bean; + } + + @Bean + public ResourceTransactionManager transactionManager(final SessionFactory sessionFactory) throws Exception { + return new HibernateTransactionManager(sessionFactory); + } + + @Bean + public ConstraintConfigDao constraintConfigDao(final SessionFactory sessionFactory) { + return new ConstraintConfigDao(sessionFactory); + } + +} diff --git a/src/test/java/org/nrg/jobtemplates/config/MockConfig.java b/src/test/java/org/nrg/jobtemplates/config/MockConfig.java new file mode 100644 index 0000000..2ae4ea0 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/MockConfig.java @@ -0,0 +1,106 @@ +package org.nrg.jobtemplates.config; + +import org.mockito.Mockito; +import org.nrg.framework.services.SerializerService; +import org.nrg.jobtemplates.repositories.ComputeSpecConfigDao; +import org.nrg.jobtemplates.repositories.HardwareConfigDao; +import org.nrg.jobtemplates.services.*; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +@Configuration +public class MockConfig { + + @Bean + public NamedParameterJdbcTemplate mockNamedParameterJdbcTemplate() { + return Mockito.mock(NamedParameterJdbcTemplate.class); + } + + @Bean + public UserManagementServiceI mockUserManagementServiceI() { + return Mockito.mock(UserManagementServiceI.class); + } + + @Bean + @Qualifier("mockRoleService") + public RoleServiceI mockRoleService() { + return Mockito.mock(RoleServiceI.class); + } + + @Bean + public RoleHolder mockRoleHolder(@Qualifier("mockRoleService") final RoleServiceI mockRoleService, + final NamedParameterJdbcTemplate mockNamedParameterJdbcTemplate) { + return new RoleHolder(mockRoleService, mockNamedParameterJdbcTemplate); + } + + @Bean + public XFTManagerHelper mockXFTManagerHelper() { + return Mockito.mock(XFTManagerHelper.class); + } + + @Bean + public SerializerService mockSerializerService() { + return Mockito.mock(SerializerService.class); + } + + @Bean + public XnatAppInfo mockXnatAppInfo() { + return Mockito.mock(XnatAppInfo.class); + } + + @Bean + public ConstraintConfigService mockPlacementConstraintConfigService() { + return Mockito.mock(ConstraintConfigService.class); + } + + @Bean + public ComputeSpecConfigService mockComputeSpecConfigService() { + return Mockito.mock(ComputeSpecConfigService.class); + } + + @Bean + @Qualifier("mockComputeSpecConfigEntityService") + public ComputeSpecConfigEntityService mockComputeSpecConfigEntityService() { + return Mockito.mock(ComputeSpecConfigEntityService.class); + } + + @Bean + public HardwareConfigService mockHardwareConfigService() { + return Mockito.mock(HardwareConfigService.class); + } + + @Bean + @Qualifier("mockHardwareConfigEntityService") + public HardwareConfigEntityService mockHardwareConfigEntityService() { + return Mockito.mock(HardwareConfigEntityService.class); + } + + @Bean + @Qualifier("mockHardwareConfigDao") + public HardwareConfigDao mockHardwareConfigDao() { + return Mockito.mock(HardwareConfigDao.class); + } + + @Bean + @Qualifier("mockComputeSpecConfigDao") + public ComputeSpecConfigDao mockComputeSpecConfigDao() { + return Mockito.mock(ComputeSpecConfigDao.class); + } + + @Bean + public JobTemplateService mockJobTemplateService() { + return Mockito.mock(JobTemplateService.class); + } + + @Bean + public ConstraintConfigEntityService mockConstraintConfigEntityService() { + return Mockito.mock(ConstraintConfigEntityService.class); + } +} diff --git a/src/test/java/org/nrg/jobtemplates/config/ObjectMapperConfig.java b/src/test/java/org/nrg/jobtemplates/config/ObjectMapperConfig.java new file mode 100644 index 0000000..c3776b5 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/ObjectMapperConfig.java @@ -0,0 +1,29 @@ +package org.nrg.jobtemplates.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.guava.GuavaModule; +import com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +@Configuration +public class ObjectMapperConfig { + @Bean + public Jackson2ObjectMapperBuilder objectMapperBuilder() { + return new Jackson2ObjectMapperBuilder() + .serializationInclusion(JsonInclude.Include.NON_NULL) + .failOnEmptyBeans(false) + .featuresToEnable(JsonParser.Feature.ALLOW_SINGLE_QUOTES, JsonParser.Feature.ALLOW_YAML_COMMENTS) + .featuresToDisable(SerializationFeature.FAIL_ON_EMPTY_BEANS, SerializationFeature.WRITE_NULL_MAP_VALUES) + .modulesToInstall(new Hibernate4Module(), new GuavaModule()); + } + + @Bean + public ObjectMapper objectMapper() { + return objectMapperBuilder().build(); + } +} diff --git a/src/test/java/org/nrg/jobtemplates/config/RestApiTestConfig.java b/src/test/java/org/nrg/jobtemplates/config/RestApiTestConfig.java new file mode 100644 index 0000000..15437fd --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/config/RestApiTestConfig.java @@ -0,0 +1,86 @@ +package org.nrg.jobtemplates.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.mockito.Mockito; +import org.nrg.mail.services.MailService; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnat.services.XnatAppInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.mockito.Mockito.when; + +@Configuration +@Import({ObjectMapperConfig.class}) +public class RestApiTestConfig extends WebMvcConfigurerAdapter { + @Bean + @Qualifier("mockXnatAppInfo") + public XnatAppInfo mockAppInfo() { + XnatAppInfo mockXnatAppInfo = Mockito.mock(XnatAppInfo.class); + when(mockXnatAppInfo.isPrimaryNode()).thenReturn(true); + return mockXnatAppInfo; + } + + @Bean + public ExecutorService executorService() { + return Executors.newCachedThreadPool(); + } + + @Bean + public ThreadPoolExecutorFactoryBean syncThreadPoolExecutorFactoryBean(ExecutorService executorService) { + ThreadPoolExecutorFactoryBean tBean = Mockito.mock(ThreadPoolExecutorFactoryBean.class); + when(tBean.getObject()).thenReturn(executorService); + return tBean; + } + + @Bean + public MailService mockMailService() { + return Mockito.mock(MailService.class); + } + + @Bean + @Qualifier("mockRoleService") + public RoleServiceI mockRoleService() { + return Mockito.mock(RoleServiceI.class); + } + + @Bean + public RoleHolder mockRoleHolder(@Qualifier("mockRoleService") final RoleServiceI roleServiceI, + final NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + return new RoleHolder(roleServiceI, namedParameterJdbcTemplate); + } + + @Bean + public NamedParameterJdbcTemplate mockNamedParameterJdbcTemplate() { + return Mockito.mock(NamedParameterJdbcTemplate.class); + } + + @Bean + public UserManagementServiceI mockUserManagementService() { + return Mockito.mock(UserManagementServiceI.class); + } + + @Override + public void configureMessageConverters(List> converters) { + converters.add(new StringHttpMessageConverter()); + converters.add(new MappingJackson2HttpMessageConverter(objectMapper)); + } + + @Autowired + private ObjectMapper objectMapper; +} diff --git a/src/test/java/org/nrg/jobtemplates/models/ConstraintTest.java b/src/test/java/org/nrg/jobtemplates/models/ConstraintTest.java new file mode 100644 index 0000000..10beb91 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/models/ConstraintTest.java @@ -0,0 +1,34 @@ +package org.nrg.jobtemplates.models; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashSet; + +import static org.junit.Assert.*; + +public class ConstraintTest { + + @Test + public void testListConversion_IN() { + Constraint constraint = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + assertEquals(Arrays.asList("node.instance.type==spot", "node.instance.type==demand"), constraint.toList()); + } + + @Test + public void testListConversion_NOT_IN() { + Constraint constraint = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + assertEquals(Arrays.asList("node.instance.type!=spot", "node.instance.type!=demand"), constraint.toList()); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApiTest.java b/src/test/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApiTest.java new file mode 100644 index 0000000..1afd407 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApiTest.java @@ -0,0 +1,232 @@ +package org.nrg.jobtemplates.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.jobtemplates.config.ComputeSpecConfigsApiConfig; +import org.nrg.jobtemplates.models.ComputeSpecConfig; +import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xft.security.UserI; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.util.NestedServletException; + +import java.util.Optional; + +import static org.mockito.Mockito.*; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebAppConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {ComputeSpecConfigsApiConfig.class}) +public class ComputeSpecConfigsApiTest { + + @Autowired private WebApplicationContext wac; + @Autowired private ObjectMapper mapper; + @Autowired private RoleServiceI mockRoleService; + @Autowired private UserManagementServiceI mockUserManagementService; + @Autowired private ComputeSpecConfigService mockComputeSpecConfigService; + + private MockMvc mockMvc; + private UserI mockUser; + private Authentication mockAuthentication; + + private ComputeSpecConfig computeSpecConfig; + + @Before + public void before() throws Exception { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); + + // Mock the user + mockUser = Mockito.mock(UserI.class); + when(mockUser.getLogin()).thenReturn("mockUser"); + when(mockUser.getEmail()).thenReturn("mockUser@mockuser.com"); + when(mockUser.getPassword()).thenReturn("mockUserPassword"); + when(mockUser.getID()).thenReturn(1); + when(mockRoleService.isSiteAdmin(mockUser)).thenReturn(true); + mockAuthentication = new TestingAuthenticationToken(mockUser, mockUser.getPassword()); + + // Setup the compute spec config + computeSpecConfig = new ComputeSpecConfig(); + computeSpecConfig.setId(1L); + } + + @After + public void after() throws Exception { + Mockito.reset( + mockRoleService, + mockUserManagementService, + mockComputeSpecConfigService, + mockUser + ); + } + + @Test + public void testGetAllComputeSpecConfigs() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/job-templates/compute-spec-configs") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify that the service was called + verify(mockComputeSpecConfigService, times(1)).getAll(); + verify(mockComputeSpecConfigService, never()).getByType(any()); + } + + @Test + public void testGetComputeSpecConfigsByType() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/job-templates/compute-spec-configs") + .param("type", "JUPYTERHUB") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify that the service was called + verify(mockComputeSpecConfigService, never()).getAll(); + verify(mockComputeSpecConfigService, times(1)).getByType(any()); + } + + @Test + public void testGetComputeSpecConfigById() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/job-templates/compute-spec-configs/1") + .accept(APPLICATION_JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify that the service was called + verify(mockComputeSpecConfigService, times(1)).retrieve(any()); + } + + @Test + public void testGetComputeSpecConfigByIdNotFound() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/job-templates/compute-spec-configs/1") + .accept(APPLICATION_JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.empty()); + + mockMvc.perform(request).andExpect(status().isNotFound()); + + // Verify that the service was called + verify(mockComputeSpecConfigService, times(1)).retrieve(any()); + } + + @Test + public void testCreateComputeSpecConfig() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .post("/job-templates/compute-spec-configs") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(mapper.writeValueAsString(new ComputeSpecConfig())) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isCreated()); + + // Verify that the service was called + verify(mockComputeSpecConfigService, times(1)).create(any()); + } + + @Test + public void testUpdateComputeSpecConfig() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .put("/job-templates/compute-spec-configs/1") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(mapper.writeValueAsString(computeSpecConfig)) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify that the service was called + verify(mockComputeSpecConfigService, times(1)).update(eq(computeSpecConfig)); + } + + @Test(expected = NestedServletException.class) + public void testUpdateComputeSpecConfig_IdMismatch() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .put("/job-templates/compute-spec-configs/2") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(mapper.writeValueAsString(computeSpecConfig)) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + // Throws NestedServletException in response to IllegalArgumentException + mockMvc.perform(request).andExpect(status().isBadRequest()); + + // Verify that the service was not called + verify(mockComputeSpecConfigService, never()).update(any()); + } + + @Test + public void testDeleteComputeSpecConfig() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .delete("/job-templates/compute-spec-configs/1") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isNoContent()); + + // Verify that the service was called + verify(mockComputeSpecConfigService, times(1)).delete(eq(1L)); + } + + @Test + public void testGetAvailableComputeSpecConfigs() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/job-templates/compute-spec-configs/available") + .param("user", mockUser.getLogin()) + .param("project", "projectId") + .param("type", "JUPYTERHUB") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify that the service was called + verify(mockComputeSpecConfigService, times(1)).getAvailable(eq(mockUser.getLogin()), eq("projectId"), eq(ComputeSpecConfig.ConfigType.JUPYTERHUB)); + } + + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/jobtemplates/rest/ConstraintConfigsApiTest.java b/src/test/java/org/nrg/jobtemplates/rest/ConstraintConfigsApiTest.java new file mode 100644 index 0000000..fb39607 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/rest/ConstraintConfigsApiTest.java @@ -0,0 +1,206 @@ +package org.nrg.jobtemplates.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.jobtemplates.config.ConstraintConfigsApiTestConfig; +import org.nrg.jobtemplates.models.ConstraintConfig; +import org.nrg.jobtemplates.services.ConstraintConfigService; +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xft.security.UserI; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.util.NestedServletException; + +import java.util.Optional; + +import static org.mockito.Mockito.*; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebAppConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {ConstraintConfigsApiTestConfig.class}) +public class ConstraintConfigsApiTest { + + @Autowired private WebApplicationContext wac; + @Autowired private ObjectMapper mapper; + @Autowired private RoleServiceI mockRoleService; + @Autowired private UserManagementServiceI mockUserManagementService; + @Autowired private ConstraintConfigService mockConstraintConfigService; + + private MockMvc mockMvc; + private UserI mockUser; + private Authentication mockAuthentication; + + private ConstraintConfig constraintConfig; + + @Before + public void before() { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); + + // Mock the user + mockUser = Mockito.mock(UserI.class); + when(mockUser.getLogin()).thenReturn("mockUser"); + when(mockUser.getEmail()).thenReturn("mockUser@mockuser.com"); + when(mockUser.getPassword()).thenReturn("mockUserPassword"); + when(mockUser.getID()).thenReturn(1); + when(mockRoleService.isSiteAdmin(mockUser)).thenReturn(true); + mockAuthentication = new TestingAuthenticationToken(mockUser, mockUser.getPassword()); + + // Set up a PlacementConstraintConfig + constraintConfig = new ConstraintConfig(); + constraintConfig.setId(1L); + } + + @After + public void after() { + Mockito.reset( + mockRoleService, + mockUserManagementService, + mockConstraintConfigService + ); + } + + @Test + public void testGetPlacementConstraintConfig() throws Exception { + // Set up the request + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/job-templates/constraint-configs/1") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + // Set up the mocks + when(mockConstraintConfigService.retrieve(1L)).thenReturn(Optional.of(constraintConfig)); + + // Make the call + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify the mocks + verify(mockConstraintConfigService).retrieve(1L); + } + + @Test + public void testGetPlacementConstraintConfigNotFound() throws Exception { + // Set up the request + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/job-templates/constraint-configs/1") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + // Set up the mocks + when(mockConstraintConfigService.retrieve(1L)).thenReturn(Optional.empty()); + + // Make the call + mockMvc.perform(request).andExpect(status().isNotFound()); + + // Verify the mocks + verify(mockConstraintConfigService).retrieve(1L); + } + + @Test + public void testGetAllPlacementConstraintConfigs() throws Exception { + // Set up the request + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/job-templates/constraint-configs") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + // Make the call + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify the mocks + verify(mockConstraintConfigService).getAll(); + } + + @Test + public void testCreatePlacementConstraintConfig() throws Exception { + // Set up the request + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .post("/job-templates/constraint-configs") + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()) + .content(mapper.writeValueAsString(constraintConfig)); + + // Make the call + mockMvc.perform(request).andExpect(status().isCreated()); + + // Verify the mocks + verify(mockConstraintConfigService).create(constraintConfig); + } + + @Test + public void testUpdatePlacementConstraintConfig() throws Exception { + // Set up the request + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .put("/job-templates/constraint-configs/1") + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()) + .content(mapper.writeValueAsString(constraintConfig)); + + // Make the call + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify the mocks + verify(mockConstraintConfigService).update(constraintConfig); + } + + @Test(expected = NestedServletException.class) + public void testUpdatePlacementConstraintConfig_IdMismatch() throws Exception { + // Set up the request + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .put("/job-templates/constraint-configs/2") + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()) + .content(mapper.writeValueAsString(constraintConfig)); + + // Make the call + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify the mocks + verify(mockConstraintConfigService, never()).update(constraintConfig); + } + + @Test + public void testDeletePlacementConstraintConfig() throws Exception { + // Set up the request + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .delete("/job-templates/constraint-configs/1") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + // Make the call + mockMvc.perform(request).andExpect(status().isNoContent()); + + // Verify the mocks + verify(mockConstraintConfigService).delete(1L); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/jobtemplates/rest/HardwareConfigsApiTest.java b/src/test/java/org/nrg/jobtemplates/rest/HardwareConfigsApiTest.java new file mode 100644 index 0000000..d132529 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/rest/HardwareConfigsApiTest.java @@ -0,0 +1,201 @@ +package org.nrg.jobtemplates.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.jobtemplates.config.HardwareConfigsApiConfig; +import org.nrg.jobtemplates.models.HardwareConfig; +import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xft.security.UserI; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.util.NestedServletException; + +import java.util.Optional; + +import static org.mockito.Mockito.*; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebAppConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {HardwareConfigsApiConfig.class}) +public class HardwareConfigsApiTest { + + @Autowired private WebApplicationContext wac; + @Autowired private ObjectMapper mapper; + @Autowired private RoleServiceI mockRoleService; + @Autowired private UserManagementServiceI mockUserManagementService; + @Autowired private HardwareConfigService mockHardwareConfigService; + + private MockMvc mockMvc; + private UserI mockUser; + private Authentication mockAuthentication; + + private HardwareConfig hardwareConfig; + + @Before + public void before() throws Exception { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); + + // Mock the user + mockUser = Mockito.mock(UserI.class); + when(mockUser.getLogin()).thenReturn("mockUser"); + when(mockUser.getEmail()).thenReturn("mockUser@mockuser.com"); + when(mockUser.getPassword()).thenReturn("mockUserPassword"); + when(mockUser.getID()).thenReturn(1); + when(mockRoleService.isSiteAdmin(mockUser)).thenReturn(true); + mockAuthentication = new TestingAuthenticationToken(mockUser, mockUser.getPassword()); + + // Set up a hardware config + hardwareConfig = new HardwareConfig(); + hardwareConfig.setId(1L); + } + + @After + public void after() throws Exception { + // Reset the mock + Mockito.reset( + mockRoleService, + mockUserManagementService, + mockHardwareConfigService, + mockUser + ); + } + + @Test + public void testGetAllHardwareConfigs() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/job-templates/hardware-configs") + .accept(APPLICATION_JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify that the service was called + verify(mockHardwareConfigService, times(1)).retrieveAll(); + } + + @Test + public void testGetHardwareConfigById() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/job-templates/hardware-configs/1") + .accept(APPLICATION_JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + when(mockHardwareConfigService.retrieve(1L)).thenReturn(Optional.of(hardwareConfig)); + + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify that the service was called + verify(mockHardwareConfigService, times(1)).retrieve(1L); + } + + @Test + public void testGetHardwareConfigByIdNotFound() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/job-templates/hardware-configs/1") + .accept(APPLICATION_JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + when(mockHardwareConfigService.retrieve(1L)).thenReturn(Optional.empty()); + + mockMvc.perform(request).andExpect(status().isNotFound()); + + // Verify that the service was called + verify(mockHardwareConfigService, times(1)).retrieve(1L); + } + + @Test + public void testCreateHardwareConfig() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .post("/job-templates/hardware-configs") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(mapper.writeValueAsString(hardwareConfig)) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + when(mockHardwareConfigService.create(hardwareConfig)).thenReturn(hardwareConfig); + + mockMvc.perform(request).andExpect(status().isCreated()); + + // Verify that the service was called + verify(mockHardwareConfigService, times(1)).create(hardwareConfig); + } + + @Test + public void testUpdateHardwareConfig() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .put("/job-templates/hardware-configs/1") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(mapper.writeValueAsString(hardwareConfig)) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + when(mockHardwareConfigService.update(hardwareConfig)).thenReturn(hardwareConfig); + + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify that the service was called + verify(mockHardwareConfigService, times(1)).update(hardwareConfig); + } + + @Test(expected = NestedServletException.class) + public void testUpdateHardwareConfig_IdMismatch() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .put("/job-templates/hardware-configs/2") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(mapper.writeValueAsString(hardwareConfig)) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isBadRequest()); + + // Verify that the service was not called + verify(mockHardwareConfigService, never()).update(hardwareConfig); + } + + @Test + public void testDeleteHardwareConfig() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .delete("/job-templates/hardware-configs/1") + .accept(APPLICATION_JSON) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isNoContent()); + + // Verify that the service was called + verify(mockHardwareConfigService, times(1)).delete(1L); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigServiceTest.java b/src/test/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigServiceTest.java new file mode 100644 index 0000000..8e493db --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigServiceTest.java @@ -0,0 +1,735 @@ +package org.nrg.jobtemplates.services.impl; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.nrg.framework.constants.Scope; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.jobtemplates.config.DefaultComputeSpecConfigServiceTestConfig; +import org.nrg.jobtemplates.entities.HardwareConfigEntity; +import org.nrg.jobtemplates.models.*; +import org.nrg.jobtemplates.services.HardwareConfigEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.transaction.Transactional; +import java.util.*; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.*; +import static org.nrg.framework.constants.Scope.*; +import static org.nrg.jobtemplates.models.ComputeSpecConfig.ConfigType.CONTAINER_SERVICE; +import static org.nrg.jobtemplates.models.ComputeSpecConfig.ConfigType.JUPYTERHUB; +import static org.nrg.jobtemplates.utils.TestingUtils.commitTransaction; + +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +@ContextConfiguration(classes = DefaultComputeSpecConfigServiceTestConfig.class) +public class DefaultComputeSpecConfigServiceTest { + + @Autowired private DefaultComputeSpecConfigService defaultComputeSpecConfigService; + @Autowired private HardwareConfigEntityService hardwareConfigEntityService; + + private ComputeSpecConfig computeSpecConfig1; + private ComputeSpecConfig computeSpecConfig2; + private ComputeSpecConfig computeSpecConfig3; + private ComputeSpecConfig computeSpecConfigInvalid; + + private HardwareConfig hardwareConfig1; + private HardwareConfig hardwareConfig2; + + @Before + public void before() { + createDummyConfigs(); + } + + @Test + @DirtiesContext + public void testExists() { + // Test + ComputeSpecConfig created = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + boolean exists = defaultComputeSpecConfigService.exists(created.getId()); + + // Verify + assertTrue(exists); + } + + @Test + @DirtiesContext + public void testDoesNotExist() { + // Test + ComputeSpecConfig created = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + boolean exists = defaultComputeSpecConfigService.exists(created.getId() + 1); + + // Verify + assertFalse(exists); + } + + @Test + @DirtiesContext + public void testGetDoesNotExist() { + // Test + Optional computeSpecConfig = defaultComputeSpecConfigService.retrieve(1L); + + // Verify + assertFalse(computeSpecConfig.isPresent()); + } + + @Test + @DirtiesContext + public void testGet() { + // Test + ComputeSpecConfig created = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + Optional computeSpecConfig = defaultComputeSpecConfigService.retrieve(created.getId()); + + // Verify + assertTrue(computeSpecConfig.isPresent()); + assertEquals(created, computeSpecConfig.get()); + + assertEquals(computeSpecConfig1.getComputeSpec(), computeSpecConfig.get().getComputeSpec()); + assertEquals(computeSpecConfig1.getConfigTypes(), computeSpecConfig.get().getConfigTypes()); + assertEquals(computeSpecConfig1.getScopes(), computeSpecConfig.get().getScopes()); + } + + @Test + @DirtiesContext + public void testGetAll() { + // Test + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + commitTransaction(); + ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + commitTransaction(); + + List computeSpecConfigs = defaultComputeSpecConfigService.getAll(); + + // Verify + assertThat(computeSpecConfigs.size(), is(3)); + assertThat(computeSpecConfigs, hasItems(created1, created2, created3)); + } + + @Test + @DirtiesContext + public void testGetByType() { + // Create ComputeSpecConfigs + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + commitTransaction(); + ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + commitTransaction(); + + // Test + List computeSpecConfigs = defaultComputeSpecConfigService.getByType(CONTAINER_SERVICE); + + // Verify + assertThat(computeSpecConfigs.size(), is(2)); + assertThat(computeSpecConfigs, hasItems(created2, created3)); + + // Test + computeSpecConfigs = defaultComputeSpecConfigService.getByType(JUPYTERHUB); + + // Verify + assertEquals(2, computeSpecConfigs.size()); + assertThat(computeSpecConfigs, containsInAnyOrder(created1, created3)); + } + + @Test + @DirtiesContext + public void testGetAvailable_WrongUser() { + // Create ComputeSpecConfigs + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + commitTransaction(); + ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + commitTransaction(); + + // Test + List computeSpecConfigs = defaultComputeSpecConfigService.getAvailable("User2", "Project1", null); + + // Verify + assertThat(computeSpecConfigs.size(), is(1)); + assertThat(computeSpecConfigs, hasItems(created1)); + } + + @Test + @DirtiesContext + public void testGetAvailable_WrongProject() { + // Create ComputeSpecConfigs + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + commitTransaction(); + ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + commitTransaction(); + + // Test + List computeSpecConfigs = defaultComputeSpecConfigService.getAvailable("User1", "Project2"); + + // Verify + assertThat(computeSpecConfigs.size(), is(1)); + assertThat(computeSpecConfigs, hasItem(created1)); + } + + @Test + @DirtiesContext + public void testGetAvailable() { + // Create ComputeSpecConfigs + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + commitTransaction(); + ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + commitTransaction(); + + // Test + List computeSpecConfigs = defaultComputeSpecConfigService.getAvailable("User1", "Project1"); + + // Verify + assertThat(computeSpecConfigs.size(), is(2)); + assertThat(computeSpecConfigs, hasItems(created1, created2)); + } + + @Test + @DirtiesContext + public void testGetAvailable_SpecificType() { + // Create ComputeSpecConfigs + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + commitTransaction(); + ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + commitTransaction(); + + // Test + List computeSpecConfigs = defaultComputeSpecConfigService.getAvailable("User1", "Project1", CONTAINER_SERVICE); + + // Verify + assertThat(computeSpecConfigs.size(), is(1)); + assertThat(computeSpecConfigs, hasItems(created2)); + } + + @Test + @DirtiesContext + public void testIsAvailable() { + // Create ComputeSpecConfigs + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + commitTransaction(); + ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + commitTransaction(); + + // Test + boolean result = defaultComputeSpecConfigService.isAvailable("User1", "Project1", created1.getId()); + + // Verify + assertTrue(result); + + // Test + result = defaultComputeSpecConfigService.isAvailable("User1", "Project1", created2.getId()); + + // Verify + assertTrue(result); + + // Test + result = defaultComputeSpecConfigService.isAvailable("User1", "Project1", created3.getId()); + + // Verify + assertFalse(result); + } + + @Test + @DirtiesContext + public void testNotAvailable_WrongUser() { + // Create ComputeSpecConfigs + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + commitTransaction(); + ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + commitTransaction(); + + // Test + boolean result = defaultComputeSpecConfigService.isAvailable("User2", "Project1", created1.getId()); + + // Verify + assertTrue(result); + + // Test + result = defaultComputeSpecConfigService.isAvailable("User2", "Project1", created2.getId()); + + // Verify + assertFalse(result); + + // Test + result = defaultComputeSpecConfigService.isAvailable("User2", "Project1", created3.getId()); + + // Verify + assertFalse(result); + } + + @Test + @DirtiesContext + public void testNotAvailable_WrongProject() { + // Create ComputeSpecConfigs + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + commitTransaction(); + ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + commitTransaction(); + + // Test + boolean result = defaultComputeSpecConfigService.isAvailable("User1", "Project2", created1.getId()); + + // Verify + assertTrue(result); + + // Test + result = defaultComputeSpecConfigService.isAvailable("User1", "Project2", created2.getId()); + + // Verify + assertFalse(result); + + // Test + result = defaultComputeSpecConfigService.isAvailable("User1", "Project2", created3.getId()); + + // Verify + assertFalse(result); + } + + @Test + @DirtiesContext + public void testCreate_AllowAllHardware() { + // First create hardware configs + HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); + HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); + commitTransaction(); + + hardwareConfig1.setId(hardwareConfigEntity1.getId()); + hardwareConfig2.setId(hardwareConfigEntity2.getId()); + + // Next create compute spec config + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + + // Verify that all hardware configs are associated with the compute spec config + assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(2)); + assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2)); + } + + @Test + @DirtiesContext + public void testCreate_SelectHardware() { + // First create hardware configs + HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); + HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); + commitTransaction(); + + hardwareConfig1.setId(hardwareConfigEntity1.getId()); + hardwareConfig2.setId(hardwareConfigEntity2.getId()); + + // Next create compute spec config + ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + commitTransaction(); + + // Verify that only the selected hardware config is associated with the compute spec config + assertThat(created2.getHardwareOptions().getHardwareConfigs().size(), is(1)); + assertThat(created2.getHardwareOptions().getHardwareConfigs(), hasItem(hardwareConfig2)); + } + + @Test + @DirtiesContext + public void testUpdate() throws NotFoundException { + // First create hardware configs + HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); + HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); + commitTransaction(); + + hardwareConfig1.setId(hardwareConfigEntity1.getId()); + hardwareConfig2.setId(hardwareConfigEntity2.getId()); + + // Next create compute spec config + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + + // Verify that all hardware configs are associated with the compute spec config + assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(2)); + assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2)); + + // Update the compute spec config + created1.getHardwareOptions().setAllowAllHardware(false); + created1.getHardwareOptions().getHardwareConfigs().remove(hardwareConfig1); + + // Update other fields + created1.getComputeSpec().setName("UpdatedName"); + created1.getComputeSpec().setImage("UpdatedImage"); + created1.getComputeSpec().getEnvironmentVariables().add(new EnvironmentVariable("UpdatedKey", "UpdatedValue")); + + // Update the compute spec config + defaultComputeSpecConfigService.update(created1); + commitTransaction(); + + // Verify that only the selected hardware config is associated with the compute spec config + assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(1)); + assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItem(hardwareConfig2)); + + // Verify that the other fields were updated + assertThat(created1.getComputeSpec().getName(), is("UpdatedName")); + assertThat(created1.getComputeSpec().getImage(), is("UpdatedImage")); + assertThat(created1.getComputeSpec().getEnvironmentVariables(), hasItem(new EnvironmentVariable("UpdatedKey", "UpdatedValue"))); + } + + @Test + @DirtiesContext + public void testDelete() throws Exception { + // First create hardware configs + HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); + HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); + commitTransaction(); + + hardwareConfig1.setId(hardwareConfigEntity1.getId()); + hardwareConfig2.setId(hardwareConfigEntity2.getId()); + + // Next create compute spec config + ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + commitTransaction(); + + // Verify that all hardware configs are associated with the compute spec config + assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(2)); + assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2)); + + hardwareConfigEntity1 = hardwareConfigEntityService.retrieve(hardwareConfigEntity1.getId()); + hardwareConfigEntity2 = hardwareConfigEntityService.retrieve(hardwareConfigEntity2.getId()); + + assertThat(hardwareConfigEntity1.getComputeSpecHardwareOptions().size(), is(1)); + assertThat(hardwareConfigEntity2.getComputeSpecHardwareOptions().size(), is(1)); + + // Delete the compute spec config + defaultComputeSpecConfigService.delete(created1.getId()); + commitTransaction(); + + // Verify that the compute spec config was deleted + assertThat(defaultComputeSpecConfigService.exists(created1.getId()), is(false)); + + // Verify that the hardware config entities were deleted + hardwareConfigEntity1 = hardwareConfigEntityService.retrieve(hardwareConfigEntity1.getId()); + hardwareConfigEntity2 = hardwareConfigEntityService.retrieve(hardwareConfigEntity2.getId()); + + assertThat(hardwareConfigEntity1.getComputeSpecHardwareOptions().size(), is(0)); + assertThat(hardwareConfigEntity2.getComputeSpecHardwareOptions().size(), is(0)); + } + + @Test + public void testValidate() { + try { + defaultComputeSpecConfigService.validate(computeSpecConfigInvalid); + fail("Expected exception to be thrown"); + } catch (IllegalArgumentException e) { + // Verify that the exception message contains the expected validation errors + // Note: the order of the validation errors is not guaranteed + // Trying not to be too brittle here + assertThat(e.getMessage(), containsString("name cannot be blank")); + assertThat(e.getMessage(), containsString("image cannot be blank")); + assertThat(e.getMessage(), containsString("must have at least one scope")); + assertThat(e.getMessage(), containsString("hardware configs cannot be null")); + assertThat(e.getMessage(), containsString("must have at least one config type")); + } + } + + public void createDummyConfigs() { + // Setup hardware + Hardware hardware1 = Hardware.builder() + .name("Small") + .cpuReservation(2.0) + .cpuLimit(4.0) + .memoryReservation("4G") + .memoryLimit("8G") + .build(); + + // Setup hardware constraints + Constraint hardwareConstraint1 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Collections.singletonList("worker"))) + .build(); + + Constraint hardwareConstraint2 = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + hardware1.setConstraints(Arrays.asList(hardwareConstraint1, hardwareConstraint2)); + + // Setup hardware environment variables + EnvironmentVariable hardwareEnvironmentVariable1 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); + EnvironmentVariable hardwareEnvironmentVariable2 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); + + hardware1.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable1, hardwareEnvironmentVariable2)); + + // Setup hardware generic resources + GenericResource hardwareGenericResource1 = new GenericResource("nvidia.com/gpu", "2"); + GenericResource hardwareGenericResource2 = new GenericResource("fpga.com/fpga", "1"); + + hardware1.setGenericResources(Arrays.asList(hardwareGenericResource1, hardwareGenericResource2)); + + // Setup hardware scopes + HardwareScope hardwareSiteScope1 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope1 = HardwareScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope userHardwareScope1 = HardwareScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map hardwareScopes1 = new HashMap<>(); + hardwareScopes1.put(Site, hardwareSiteScope1); + hardwareScopes1.put(Project, hardwareProjectScope1); + hardwareScopes1.put(User, userHardwareScope1); + + // Setup a hardware config entity + hardwareConfig1 = HardwareConfig.builder() + .hardware(hardware1) + .scopes(hardwareScopes1) + .build(); + + // Setup second hardware config + Hardware hardware2 = Hardware.builder() + .name("Medium") + .cpuReservation(4.0) + .cpuLimit(4.0) + .memoryReservation("8G") + .memoryLimit("8G") + .build(); + + // Setup hardware constraints + Constraint hardwareConstraint3 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Collections.singletonList("worker"))) + .build(); + + Constraint hardwareConstraint4 = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + hardware2.setConstraints(Arrays.asList(hardwareConstraint3, hardwareConstraint4)); + + // Setup hardware environment variables + EnvironmentVariable hardwareEnvironmentVariable3 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); + EnvironmentVariable hardwareEnvironmentVariable4 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); + + hardware2.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable3, hardwareEnvironmentVariable4)); + + // Setup hardware generic resources + GenericResource hardwareGenericResource3 = new GenericResource("nvidia.com/gpu", "2"); + GenericResource hardwareGenericResource4 = new GenericResource("fpga.com/fpga", "1"); + + hardware2.setGenericResources(Arrays.asList(hardwareGenericResource3, hardwareGenericResource4)); + + // Setup hardware scopes + HardwareScope hardwareSiteScope2 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope2 = HardwareScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope userHardwareScope2 = HardwareScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map hardwareScopes2 = new HashMap<>(); + hardwareScopes2.put(Site, hardwareSiteScope2); + hardwareScopes2.put(Project, hardwareProjectScope2); + hardwareScopes2.put(User, userHardwareScope2); + + // Setup second hardware config entity + hardwareConfig2 = HardwareConfig.builder() + .hardware(hardware2) + .scopes(hardwareScopes2) + .build(); + + // Setup first compute spec + ComputeSpec computeSpec1 = ComputeSpec.builder() + .name("Jupyter Datascience Notebook") + .image("jupyter/datascience-notebook:hub-3.0.0") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeSpecScope computeSpecSiteScope1 = ComputeSpecScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeSpecScope computeSpecProjectScope1 = ComputeSpecScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeSpecScope computeSpecUserScope1 = ComputeSpecScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map computeSpecScopes1 = new HashMap<>(); + computeSpecScopes1.put(Site, computeSpecSiteScope1); + computeSpecScopes1.put(Project, computeSpecProjectScope1); + computeSpecScopes1.put(User, computeSpecUserScope1); + + ComputeSpecHardwareOptions computeSpecHardwareOptions1 = ComputeSpecHardwareOptions.builder() + .allowAllHardware(true) + .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig1, hardwareConfig2))) + .build(); + + computeSpecConfig1 = ComputeSpecConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(ComputeSpecConfig.ConfigType.JUPYTERHUB))) + .computeSpec(computeSpec1) + .scopes(computeSpecScopes1) + .hardwareOptions(computeSpecHardwareOptions1) + .build(); + + // Setup second compute spec + ComputeSpec computeSpec2 = ComputeSpec.builder() + .name("XNAT Datascience Notebook") + .image("xnat/datascience-notebook:latest") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeSpecScope computeSpecSiteScope2 = ComputeSpecScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeSpecScope computeSpecProjectScope2 = ComputeSpecScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("Project1"))) + .build(); + + ComputeSpecScope computeSpecUserScope2 = ComputeSpecScope.builder() + .scope(User) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("User1"))) + .build(); + + Map computeSpecScopes2 = new HashMap<>(); + computeSpecScopes2.put(Site, computeSpecSiteScope2); + computeSpecScopes2.put(Project, computeSpecProjectScope2); + computeSpecScopes2.put(User, computeSpecUserScope2); + + ComputeSpecHardwareOptions computeSpecHardwareOptions2 = ComputeSpecHardwareOptions.builder() + .allowAllHardware(false) + .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig2))) + .build(); + + computeSpecConfig2 = ComputeSpecConfig.builder() + .id(2L) + .configTypes(new HashSet<>(Collections.singletonList(CONTAINER_SERVICE))) + .computeSpec(computeSpec2) + .scopes(computeSpecScopes2) + .hardwareOptions(computeSpecHardwareOptions2) + .build(); + + // Setup third compute spec + ComputeSpec computeSpec3 = ComputeSpec.builder() + .name("MATLAB Datascience Notebook") + .image("matlab/datascience-notebook:latest") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeSpecScope computeSpecSiteScope3 = ComputeSpecScope.builder() + .scope(Site) + .enabled(false) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeSpecScope computeSpecProjectScope3 = ComputeSpecScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>()) + .build(); + + ComputeSpecScope computeSpecUserScope3 = ComputeSpecScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>()) + .build(); + + Map computeSpecScopes3 = new HashMap<>(); + computeSpecScopes3.put(Site, computeSpecSiteScope3); + computeSpecScopes3.put(Project, computeSpecProjectScope3); + computeSpecScopes3.put(User, computeSpecUserScope3); + + ComputeSpecHardwareOptions computeSpecHardwareOptions3 = ComputeSpecHardwareOptions.builder() + .allowAllHardware(true) + .hardwareConfigs(new HashSet<>()) + .build(); + + computeSpecConfig3 = ComputeSpecConfig.builder() + .configTypes(new HashSet<>(Arrays.asList(CONTAINER_SERVICE, JUPYTERHUB))) + .computeSpec(computeSpec3) + .scopes(computeSpecScopes3) + .hardwareOptions(computeSpecHardwareOptions3) + .build(); + + // Setup invalid compute spec config + ComputeSpec computeSpecInvalid = ComputeSpec.builder() + .name("") // invalid + .image("") // invalid + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + Map computeSpecScopesInvalid = new HashMap<>(); // invalid, no scopes + + ComputeSpecHardwareOptions computeSpecHardwareOptionsInvalid = ComputeSpecHardwareOptions.builder() + .allowAllHardware(true) + .hardwareConfigs(null) // invalid + .build(); + + computeSpecConfigInvalid = ComputeSpecConfig.builder() + .configTypes(null) // invalid + .computeSpec(computeSpecInvalid) + .scopes(computeSpecScopesInvalid) + .hardwareOptions(computeSpecHardwareOptionsInvalid) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigServiceTest.java b/src/test/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigServiceTest.java new file mode 100644 index 0000000..4439d5f --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigServiceTest.java @@ -0,0 +1,274 @@ +package org.nrg.jobtemplates.services.impl; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.nrg.framework.constants.Scope; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.jobtemplates.config.DefaultConstraintConfigServiceTestConfig; +import org.nrg.jobtemplates.models.Constraint; +import org.nrg.jobtemplates.models.ConstraintConfig; +import org.nrg.jobtemplates.models.ConstraintScope; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.transaction.Transactional; +import java.util.*; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.nrg.jobtemplates.utils.TestingUtils.commitTransaction; + +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +@ContextConfiguration(classes = DefaultConstraintConfigServiceTestConfig.class) +public class DefaultConstraintConfigServiceTest { + + @Autowired private DefaultConstraintConfigService constraintConfigService; + + private ConstraintConfig constraintConfig1; + private ConstraintConfig constraintConfig2; + private ConstraintConfig constraintConfig3; + private ConstraintConfig constraintConfigInvalid; + + @Before + public void before() { + createDummyConstraintConfigs(); + } + + @Test + @DirtiesContext + public void testRetrieve() { + // Test + ConstraintConfig created = constraintConfigService.create(constraintConfig1); + commitTransaction(); + Optional retrieved = constraintConfigService.retrieve(created.getId()); + + // Verify + assertTrue(retrieved.isPresent()); + assertEquals(created, retrieved.get()); + + constraintConfig1.setId(created.getId()); + assertThat(retrieved.get(), is(constraintConfig1)); + } + + @Test + @DirtiesContext + public void testRetrieveDoesNotExist() { + // Test + Optional retrieved = constraintConfigService.retrieve(1L); + + // Verify + assertFalse(retrieved.isPresent()); + } + + @Test + @DirtiesContext + public void testGetAll() { + // Test + ConstraintConfig created1 = constraintConfigService.create(constraintConfig1); + commitTransaction(); + ConstraintConfig created2 = constraintConfigService.create(constraintConfig2); + commitTransaction(); + ConstraintConfig created3 = constraintConfigService.create(constraintConfig3); + commitTransaction(); + + List retrieved = constraintConfigService.getAll(); + + // Verify + assertEquals(3, retrieved.size()); + assertThat(retrieved, hasItems(created1, created2, created3)); + } + + @Test + @DirtiesContext + public void testCreate() { + // Test + ConstraintConfig created = constraintConfigService.create(constraintConfig1); + commitTransaction(); + + // Verify + assertNotNull(created.getId()); + constraintConfig1.setId(created.getId()); + assertEquals(created, constraintConfig1); + } + + @Test + @DirtiesContext + public void testUpdate() throws NotFoundException { + // Test + ConstraintConfig created = constraintConfigService.create(constraintConfig1); + commitTransaction(); + created.getConstraint().setKey("newKey"); + ConstraintConfig updated = constraintConfigService.update(created); + commitTransaction(); + + // Verify + assertEquals(created, updated); + } + + @Test + @DirtiesContext + public void testDelete() { + // Test + ConstraintConfig created = constraintConfigService.create(constraintConfig1); + commitTransaction(); + constraintConfigService.delete(created.getId()); + commitTransaction(); + + // Verify + Optional retrieved = constraintConfigService.retrieve(created.getId()); + assertFalse(retrieved.isPresent()); + } + + @Test + @DirtiesContext + public void testGetAvailable() { + // Setup + ConstraintConfig created1 = constraintConfigService.create(constraintConfig1); + commitTransaction(); + ConstraintConfig created2 = constraintConfigService.create(constraintConfig2); + commitTransaction(); + ConstraintConfig created3 = constraintConfigService.create(constraintConfig3); + commitTransaction(); + + // Test + List retrieved = constraintConfigService.getAvailable("ProjectA"); + + // Verify + assertEquals(2, retrieved.size()); + assertThat(retrieved, hasItems(created1, created3)); + + // Test + retrieved = constraintConfigService.getAvailable("ProjectB"); + + // Verify + assertEquals(2, retrieved.size()); + assertThat(retrieved, hasItems(created1, created3)); + + // Test + retrieved = constraintConfigService.getAvailable("ProjectC"); + + // Verify + assertEquals(1, retrieved.size()); + assertThat(retrieved, hasItem(created1)); + } + + @Test + public void testValidate() { + try { + constraintConfigService.validate(constraintConfigInvalid); + fail("Should have thrown an exception"); + } catch (Exception e) { + assertThat(e.getMessage(), containsString("key cannot be null or blank")); + assertThat(e.getMessage(), containsString("values cannot be null or empty")); + assertThat(e.getMessage(), containsString("operator cannot be null")); + assertThat(e.getMessage(), containsString("Scopes cannot be null or empty")); + } + } + + protected void createDummyConstraintConfigs() { + createDummyConstraintConfig1(); + createDummyConstraintConfig2(); + createDummyConstraintConfig3(); + createDummyConstraintConfigInvalid(); + } + + protected void createDummyConstraintConfig1() { + constraintConfig1 = new ConstraintConfig(); + + Constraint constraint1 = new Constraint(); + constraint1.setKey("node.role"); + constraint1.setOperator(Constraint.Operator.IN); + constraint1.setValues(new HashSet<>(Collections.singletonList("worker"))); + constraintConfig1.setConstraint(constraint1); + + ConstraintScope siteScope1 = ConstraintScope.builder() + .scope(Scope.Site) + .enabled(true) + .ids(Collections.emptySet()) + .build(); + ConstraintScope projectScope1 = ConstraintScope.builder() + .scope(Scope.Project) + .enabled(true) + .ids(Collections.emptySet()) + .build(); + + Map scopes1 = new HashMap<>(); + scopes1.put(Scope.Site, siteScope1); + scopes1.put(Scope.Project, projectScope1); + + constraintConfig1.setScopes(scopes1); + } + + protected void createDummyConstraintConfig2() { + constraintConfig2 = new ConstraintConfig(); + + Constraint constraint2 = new Constraint(); + constraint2.setKey("node.role"); + constraint2.setOperator(Constraint.Operator.IN); + constraint2.setValues(new HashSet<>(Collections.singletonList("worker"))); + constraintConfig2.setConstraint(constraint2); + + ConstraintScope siteScope2 = ConstraintScope.builder() + .scope(Scope.Site) + .enabled(false) + .ids(Collections.emptySet()) + .build(); + ConstraintScope projectScope2 = ConstraintScope.builder() + .scope(Scope.Project) + .enabled(true) + .ids(Collections.emptySet()) + .build(); + + Map scopes2 = new HashMap<>(); + scopes2.put(Scope.Site, siteScope2); + scopes2.put(Scope.Project, projectScope2); + + constraintConfig2.setScopes(scopes2); + } + + protected void createDummyConstraintConfig3() { + constraintConfig3 = new ConstraintConfig(); + + Constraint constraint3 = new Constraint(); + constraint3.setKey("node.label.projects"); + constraint3.setOperator(Constraint.Operator.IN); + constraint3.setValues(new HashSet<>(Arrays.asList("ProjectA", "ProjectB"))); + constraintConfig3.setConstraint(constraint3); + + ConstraintScope siteScope3 = ConstraintScope.builder() + .scope(Scope.Site) + .enabled(true) + .ids(Collections.emptySet()) + .build(); + ConstraintScope projectScope3 = ConstraintScope.builder() + .scope(Scope.Project) + .enabled(false) + .ids(new HashSet<>(Arrays.asList("ProjectA", "ProjectB"))) + .build(); + + Map scopes3 = new HashMap<>(); + scopes3.put(Scope.Site, siteScope3); + scopes3.put(Scope.Project, projectScope3); + + constraintConfig3.setScopes(scopes3); + } + + protected void createDummyConstraintConfigInvalid() { + constraintConfigInvalid = new ConstraintConfig(); + + Constraint constraintInvalid = new Constraint(); + constraintInvalid.setKey(""); + constraintInvalid.setOperator(null); + constraintInvalid.setValues(new HashSet<>()); + constraintConfigInvalid.setConstraint(constraintInvalid); + + Map scopesInvalid = new HashMap<>(); + + constraintConfigInvalid.setScopes(scopesInvalid); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigServiceTest.java b/src/test/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigServiceTest.java new file mode 100644 index 0000000..189a3ab --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigServiceTest.java @@ -0,0 +1,471 @@ +package org.nrg.jobtemplates.services.impl; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.nrg.framework.constants.Scope; +import org.nrg.jobtemplates.config.DefaultHardwareConfigServiceTestConfig; +import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; +import org.nrg.jobtemplates.entities.HardwareConfigEntity; +import org.nrg.jobtemplates.models.*; +import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.transaction.Transactional; +import java.util.*; +import java.util.stream.Collectors; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.nrg.framework.constants.Scope.*; +import static org.nrg.jobtemplates.utils.TestingUtils.commitTransaction; + +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +@ContextConfiguration(classes = DefaultHardwareConfigServiceTestConfig.class) +public class DefaultHardwareConfigServiceTest { + + @Autowired private DefaultHardwareConfigService defaultHardwareConfigService; + @Autowired private ComputeSpecConfigEntityService computeSpecConfigEntityService; + + private ComputeSpecConfig computeSpecConfig1; + private ComputeSpecConfig computeSpecConfig2; + private HardwareConfig hardwareConfig1; + private HardwareConfig hardwareConfig2; + private HardwareConfig hardwareConfigInvalid; + + @Before + public void before() throws Exception { + createDummyConfigsAndEntities(); + } + + @Test + @DirtiesContext + public void testExists() { + // Test + HardwareConfig hardwareConfig = defaultHardwareConfigService.create(hardwareConfig1); + commitTransaction(); + boolean exists = defaultHardwareConfigService.exists(hardwareConfig.getId()); + + // Verify + assertTrue(exists); + } + + @Test + @DirtiesContext + public void testDoesNotExist() { + // Test + boolean exists = defaultHardwareConfigService.exists(3L); + + // Verify + assertFalse(exists); + } + + @Test + @DirtiesContext + public void testRetrieve() { + // Setup + HardwareConfig created = defaultHardwareConfigService.create(hardwareConfig1); + commitTransaction(); + Optional retrieved = defaultHardwareConfigService.retrieve(created.getId()); + + // Verify + assertTrue(retrieved.isPresent()); + assertThat(retrieved.get(), is(created)); + + hardwareConfig1.setId(retrieved.get().getId()); + assertThat(retrieved.get(), is(hardwareConfig1)); + } + + @Test + @DirtiesContext + public void testRetrieve_DoesNotExist() { + // Setup + Optional retrieved = defaultHardwareConfigService.retrieve(3L); + + // Verify + assertFalse(retrieved.isPresent()); + } + + @Test + @DirtiesContext + public void testRetrieveAll() { + // Setup + HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); + HardwareConfig created2 = defaultHardwareConfigService.create(hardwareConfig2); + commitTransaction(); + + // Test + List retrieved = defaultHardwareConfigService.retrieveAll(); + + // Verify + assertEquals(2, retrieved.size()); + assertThat(retrieved, hasItems(created1, created2)); + } + + @Test + @DirtiesContext + public void testCreate() { + // First, create the compute spec configs + ComputeSpecConfigEntity computeSpecConfigEntity1 = computeSpecConfigEntityService.create(ComputeSpecConfigEntity.fromPojo(computeSpecConfig1)); + ComputeSpecConfigEntity computeSpecConfigEntity2 = computeSpecConfigEntityService.create(ComputeSpecConfigEntity.fromPojo(computeSpecConfig2)); + commitTransaction(); + + // Now create a hardware config + HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); + commitTransaction(); + HardwareConfig created2 = defaultHardwareConfigService.create(hardwareConfig2); + commitTransaction(); + + // Then retrieve the compute spec configs + computeSpecConfigEntity1 = computeSpecConfigEntityService.retrieve(computeSpecConfigEntity1.getId()); + computeSpecConfigEntity2 = computeSpecConfigEntityService.retrieve(computeSpecConfigEntity2.getId()); + + // Verify that the hardware config were added only to the first compute spec config + assertThat(computeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().size(), is(2)); + assertThat(computeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().size(), is(0)); + assertThat(computeSpecConfigEntity1 + .getHardwareOptions() + .getHardwareConfigs().stream() + .map(HardwareConfigEntity::toPojo) + .collect(Collectors.toList()), + hasItems(created1, created2)); + } + + @Test + @DirtiesContext + public void testUpdate() throws Exception { + // Setup + HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); + commitTransaction(); + + // Test + created1.getHardware().setName("Updated"); + created1.getHardware().setCpuLimit(6.5); + created1.getHardware().setMemoryLimit("10G"); + + defaultHardwareConfigService.update(created1); + commitTransaction(); + + // Verify + HardwareConfig retrieved = defaultHardwareConfigService.retrieve(created1.getId()).get(); + + assertThat(retrieved, is(created1)); + assertThat(retrieved.getHardware().getName(), is("Updated")); + assertThat(retrieved.getHardware().getCpuLimit(), is(6.5)); + assertThat(retrieved.getHardware().getMemoryLimit(), is("10G")); + } + + @Test + @DirtiesContext + public void testDelete() throws Exception { + // Setup + HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); + commitTransaction(); + + // Test + defaultHardwareConfigService.delete(created1.getId()); + commitTransaction(); + + // Verify + assertFalse(defaultHardwareConfigService.exists(created1.getId())); + } + + @Test + @DirtiesContext + public void testIsAvailable() { + // Setup + HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); + HardwareConfig created2 = defaultHardwareConfigService.create(hardwareConfig2); + commitTransaction(); + + // Test + boolean isAvailable1 = defaultHardwareConfigService.isAvailable("User1", "Project1", created1.getId()); + boolean isAvailable2 = defaultHardwareConfigService.isAvailable("User1", "Project1", created2.getId()); + + // Verify + assertTrue(isAvailable1); + assertTrue(isAvailable2); + } + + @Test + @DirtiesContext + public void testIsAvailable_WrongUser() { + // Setup + HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); + HardwareConfig created2 = defaultHardwareConfigService.create(hardwareConfig2); + commitTransaction(); + + // Test + boolean isAvailable1 = defaultHardwareConfigService.isAvailable("User2", "Project1", created1.getId()); + boolean isAvailable2 = defaultHardwareConfigService.isAvailable("User2", "Project1", created2.getId()); + + // Verify + assertTrue(isAvailable1); + assertFalse(isAvailable2); + } + + @Test + @DirtiesContext + public void testIsAvailable_WrongProject() { + // Setup + HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); + HardwareConfig created2 = defaultHardwareConfigService.create(hardwareConfig2); + commitTransaction(); + + // Test + boolean isAvailable1 = defaultHardwareConfigService.isAvailable("User1", "Project2", created1.getId()); + boolean isAvailable2 = defaultHardwareConfigService.isAvailable("User1", "Project2", created2.getId()); + + // Verify + assertTrue(isAvailable1); + assertFalse(isAvailable2); + } + + @Test + public void testValidate() { + try { + defaultHardwareConfigService.validate(hardwareConfigInvalid); + fail("Expected an IllegalArgumentException to be thrown"); + } catch (IllegalArgumentException e) { + // Verify that the exception message contains the expected validation errors + // Note: the order of the validation errors is not guaranteed + // Trying not to be too brittle here + assertThat(e.getMessage(), containsString("name cannot be blank")); + assertThat(e.getMessage(), containsString("scopes cannot be null or empty")); + } + } + + public void createDummyConfigsAndEntities() { + // Setup hardware + Hardware hardware1 = Hardware.builder() + .name("Small") + .cpuReservation(2.0) + .cpuLimit(4.0) + .memoryReservation("4G") + .memoryLimit("8G") + .build(); + + // Setup hardware constraints + Constraint hardwareConstraint1 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Collections.singletonList("worker"))) + .build(); + + Constraint hardwareConstraint2 = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + hardware1.setConstraints(Arrays.asList(hardwareConstraint1, hardwareConstraint2)); + + // Setup hardware environment variables + EnvironmentVariable hardwareEnvironmentVariable1 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); + EnvironmentVariable hardwareEnvironmentVariable2 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); + + hardware1.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable1, hardwareEnvironmentVariable2)); + + // Setup hardware generic resources + GenericResource hardwareGenericResource1 = new GenericResource("nvidia.com/gpu", "2"); + GenericResource hardwareGenericResource2 = new GenericResource("fpga.com/fpga", "1"); + + hardware1.setGenericResources(Arrays.asList(hardwareGenericResource1, hardwareGenericResource2)); + + // Setup hardware scopes + HardwareScope hardwareSiteScope1 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope1 = HardwareScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope userHardwareScope1 = HardwareScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map hardwareScopes1 = new HashMap<>(); + hardwareScopes1.put(Site, hardwareSiteScope1); + hardwareScopes1.put(Project, hardwareProjectScope1); + hardwareScopes1.put(User, userHardwareScope1); + + // Build hardware config + hardwareConfig1 = HardwareConfig.builder() + .hardware(hardware1) + .scopes(hardwareScopes1) + .build(); + + // Setup second hardware config + Hardware hardware2 = Hardware.builder() + .name("Medium") + .cpuReservation(4.0) + .cpuLimit(4.0) + .memoryReservation("8G") + .memoryLimit("8G") + .build(); + + // Setup hardware constraints + Constraint hardwareConstraint3 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Collections.singletonList("worker"))) + .build(); + + Constraint hardwareConstraint4 = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + hardware2.setConstraints(Arrays.asList(hardwareConstraint3, hardwareConstraint4)); + + // Setup hardware environment variables + EnvironmentVariable hardwareEnvironmentVariable3 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); + EnvironmentVariable hardwareEnvironmentVariable4 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); + + hardware2.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable3, hardwareEnvironmentVariable4)); + + // Setup hardware generic resources + GenericResource hardwareGenericResource3 = new GenericResource("nvidia.com/gpu", "2"); + GenericResource hardwareGenericResource4 = new GenericResource("fpga.com/fpga", "1"); + + hardware2.setGenericResources(Arrays.asList(hardwareGenericResource3, hardwareGenericResource4)); + + // Setup hardware scopes + HardwareScope hardwareSiteScope2 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope2 = HardwareScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("Project1"))) + .build(); + + HardwareScope userHardwareScope2 = HardwareScope.builder() + .scope(User) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("User1"))) + .build(); + + Map hardwareScopes2 = new HashMap<>(); + hardwareScopes2.put(Site, hardwareSiteScope2); + hardwareScopes2.put(Project, hardwareProjectScope2); + hardwareScopes2.put(User, userHardwareScope2); + + // Build second hardware config + hardwareConfig2 = HardwareConfig.builder() + .hardware(hardware2) + .scopes(hardwareScopes2) + .build(); + + // Setup invalid hardware config + Hardware hardwareInvalid = Hardware.builder().build(); + Map hardwareScopesInvalid = new HashMap<>(); + hardwareConfigInvalid = HardwareConfig.builder() + .hardware(hardwareInvalid) + .scopes(hardwareScopesInvalid) + .build(); + + // Setup first compute spec + ComputeSpec computeSpec1 = ComputeSpec.builder() + .name("Jupyter Datascience Notebook") + .image("jupyter/datascience-notebook:hub-3.0.0") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeSpecScope computeSpecSiteScope1 = ComputeSpecScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeSpecScope computeSpecProjectScope1 = ComputeSpecScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeSpecScope computeSpecUserScope1 = ComputeSpecScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map computeSpecScopes1 = new HashMap<>(); + computeSpecScopes1.put(Site, computeSpecSiteScope1); + computeSpecScopes1.put(Project, computeSpecProjectScope1); + computeSpecScopes1.put(User, computeSpecUserScope1); + + ComputeSpecHardwareOptions computeSpecHardwareOptions1 = ComputeSpecHardwareOptions.builder() + .allowAllHardware(true) + .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig1, hardwareConfig2))) + .build(); + + computeSpecConfig1 = ComputeSpecConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(ComputeSpecConfig.ConfigType.JUPYTERHUB))) + .computeSpec(computeSpec1) + .scopes(computeSpecScopes1) + .hardwareOptions(computeSpecHardwareOptions1) + .build(); + + // Setup second compute spec + ComputeSpec computeSpec2 = ComputeSpec.builder() + .name("XNAT Datascience Notebook") + .image("xnat/datascience-notebook:latest") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeSpecScope computeSpecSiteScope2 = ComputeSpecScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeSpecScope computeSpecProjectScope2 = ComputeSpecScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("Project1"))) + .build(); + + ComputeSpecScope computeSpecUserScope2 = ComputeSpecScope.builder() + .scope(User) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("User1"))) + .build(); + + Map computeSpecScopes2 = new HashMap<>(); + computeSpecScopes2.put(Site, computeSpecSiteScope2); + computeSpecScopes2.put(Project, computeSpecProjectScope2); + computeSpecScopes2.put(User, computeSpecUserScope2); + + ComputeSpecHardwareOptions computeSpecHardwareOptions2 = ComputeSpecHardwareOptions.builder() + .allowAllHardware(false) + .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig2))) + .build(); + + computeSpecConfig2 = ComputeSpecConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(ComputeSpecConfig.ConfigType.JUPYTERHUB))) + .computeSpec(computeSpec2) + .scopes(computeSpecScopes2) + .hardwareOptions(computeSpecHardwareOptions2) + .build(); + + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateServiceTest.java b/src/test/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateServiceTest.java new file mode 100644 index 0000000..d50035c --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateServiceTest.java @@ -0,0 +1,198 @@ +package org.nrg.jobtemplates.services.impl; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.jobtemplates.config.DefaultJobTemplateServiceTestConfig; +import org.nrg.jobtemplates.models.*; +import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.jobtemplates.services.ConstraintConfigService; +import org.nrg.jobtemplates.services.HardwareConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = DefaultJobTemplateServiceTestConfig.class) +public class DefaultJobTemplateServiceTest { + + @Autowired private DefaultJobTemplateService jobTemplateService; + @Autowired private ComputeSpecConfigService mockComputeSpecConfigService; + @Autowired private HardwareConfigService mockHardwareConfigService; + @Autowired private ConstraintConfigService mockConstraintConfigService; + + @Before + public void before() { + } + + @After + public void after() { + Mockito.reset( + mockComputeSpecConfigService, + mockHardwareConfigService, + mockConstraintConfigService + ); + } + + @Test + public void testIsAvailable_ComputeSpecNotAvailable() { + // Setup + when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(false); + when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); + + // Run + boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); + + // Verify + assertFalse(isAvailable); + } + + @Test + public void testIsAvailable_HardwareNotAvailable() { + // Setup + when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(false); + + // Run + boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); + + // Verify + assertFalse(isAvailable); + } + + @Test + public void testIsAvailable_ComputeSpecAndHardwareNotAvailable() { + // Setup + when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(false); + when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(false); + + // Run + boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); + + // Verify + assertFalse(isAvailable); + } + + @Test + public void testIsAvailable_ComputeSpecConfigAllHardwareIsAvailable() { + // Setup + when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); + + ComputeSpecConfig computeSpecConfig = ComputeSpecConfig.builder().build(); + ComputeSpecHardwareOptions hardwareOptions = ComputeSpecHardwareOptions.builder().build(); + hardwareOptions.setAllowAllHardware(true); + computeSpecConfig.setHardwareOptions(hardwareOptions); + when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + + // Run + boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); + + // Verify + assertTrue(isAvailable); + } + + @Test + public void testIsAvailable_ComputeSpecConfigSpecificHardwareAllowed() { + // Setup + when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); + + ComputeSpecConfig computeSpecConfig = ComputeSpecConfig.builder().id(1L).build(); + ComputeSpecHardwareOptions hardwareOptions = ComputeSpecHardwareOptions.builder().build(); + hardwareOptions.setAllowAllHardware(false); + computeSpecConfig.setHardwareOptions(hardwareOptions); + HardwareConfig hardwareConfig = HardwareConfig.builder().id(1L).build(); + hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); + when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + + // Run + boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); + + // Verify + assertTrue(isAvailable); + } + + @Test + public void testIsAvailable_ComputeSpecConfigSpecificHardwareNotAllowed() { + // Setup + when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); + + ComputeSpecConfig computeSpecConfig = ComputeSpecConfig.builder().id(1L).build(); + ComputeSpecHardwareOptions hardwareOptions = ComputeSpecHardwareOptions.builder().build(); + hardwareOptions.setAllowAllHardware(false); + computeSpecConfig.setHardwareOptions(hardwareOptions); + HardwareConfig hardwareConfig = HardwareConfig.builder().id(1L).build(); + hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); + when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + + // Run + boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 2L); + + // Verify + assertFalse(isAvailable); + } + + @Test + public void testResolve() { + // Setup + when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); + + ComputeSpecConfig computeSpecConfig = ComputeSpecConfig.builder().id(1L).build(); + ComputeSpec computeSpec = ComputeSpec.builder() + .name("JupyterHub Data Science Notebook") + .image("jupyter/datascience-notebook:latest") + .build(); + computeSpecConfig.setComputeSpec(computeSpec); + ComputeSpecHardwareOptions hardwareOptions = ComputeSpecHardwareOptions.builder().build(); + hardwareOptions.setAllowAllHardware(false); + computeSpecConfig.setHardwareOptions(hardwareOptions); + HardwareConfig hardwareConfig = HardwareConfig.builder().id(1L).build(); + Hardware hardware = Hardware.builder() + .name("Standard") + .cpuLimit(1.0) + .memoryLimit("4G") + .build(); + hardwareConfig.setHardware(hardware); + hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); + + Constraint constraint = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Arrays.asList("worker"))) + .build(); + + ConstraintConfig constraintConfig = ConstraintConfig.builder() + .id(1L) + .constraint(constraint) + .build(); + + when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + when(mockHardwareConfigService.retrieve(any())).thenReturn(Optional.of(hardwareConfig)); + when(mockConstraintConfigService.getAvailable(any())).thenReturn(Collections.singletonList(constraintConfig)); + + // Run + JobTemplate jobTemplate = jobTemplateService.resolve("user", "project", 1L, 1L); + + // Verify + assertNotNull(jobTemplate); + assertEquals(computeSpecConfig.getComputeSpec(), jobTemplate.getComputeSpec()); + assertEquals(hardware, jobTemplate.getHardware()); + assertEquals(Collections.singletonList(constraint), jobTemplate.getConstraints()); + } + + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityServiceTest.java b/src/test/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityServiceTest.java new file mode 100644 index 0000000..29ebd94 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityServiceTest.java @@ -0,0 +1,505 @@ +package org.nrg.jobtemplates.services.impl; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.framework.constants.Scope; +import org.nrg.jobtemplates.config.HibernateComputeSpecConfigEntityServiceTestConfig; +import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; +import org.nrg.jobtemplates.entities.HardwareConfigEntity; +import org.nrg.jobtemplates.models.*; +import org.nrg.jobtemplates.repositories.ComputeSpecConfigDao; +import org.nrg.jobtemplates.repositories.HardwareConfigDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.transaction.Transactional; +import java.util.*; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; +import static org.nrg.framework.constants.Scope.*; +import static org.nrg.jobtemplates.models.ComputeSpecConfig.ConfigType.CONTAINER_SERVICE; +import static org.nrg.jobtemplates.models.ComputeSpecConfig.ConfigType.JUPYTERHUB; +import static org.nrg.jobtemplates.utils.TestingUtils.commitTransaction; + +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +@ContextConfiguration(classes = HibernateComputeSpecConfigEntityServiceTestConfig.class) +public class HibernateComputeSpecConfigEntityServiceTest { + + @Autowired private HibernateComputeSpecConfigEntityService hibernateComputeSpecConfigEntityService; + @Autowired @Qualifier("hardwareConfigDaoImpl") private HardwareConfigDao hardwareConfigDaoImpl; + @Autowired @Qualifier("computeSpecConfigDaoImpl") private ComputeSpecConfigDao computeSpecConfigDaoImpl; + + private ComputeSpecConfig computeSpecConfig1; + private ComputeSpecConfig computeSpecConfig2; + private ComputeSpecConfig computeSpecConfig3; + + private HardwareConfig hardwareConfig1; + private HardwareConfig hardwareConfig2; + + @Before + public void before() { + hibernateComputeSpecConfigEntityService.setDao(computeSpecConfigDaoImpl); + createDummyConfigsAndEntities(); + } + + @After + public void after() { + Mockito.reset(); + } + + @Test + public void test() { + assertNotNull(hibernateComputeSpecConfigEntityService); + assertNotNull(hardwareConfigDaoImpl); + assertNotNull(computeSpecConfigDaoImpl); + } + + @Test + @DirtiesContext + public void testCreate() { + // Setup + ComputeSpecConfigEntity computeSpecConfigEntity1 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig1); + + // Execute + ComputeSpecConfigEntity created = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity1); + commitTransaction(); + ComputeSpecConfigEntity retrieved = hibernateComputeSpecConfigEntityService.retrieve(created.getId()); + + // Verify + assertNotNull(retrieved); + assertThat(retrieved.toPojo(), is(created.toPojo())); + } + + @Test + @DirtiesContext + public void testAddHardwareConfigEntity() { + // Setup + HardwareConfigEntity hardwareConfigEntity1 = HardwareConfigEntity.fromPojo(hardwareConfig1); + HardwareConfigEntity hardwareConfigEntity2 = HardwareConfigEntity.fromPojo(hardwareConfig2); + ComputeSpecConfigEntity computeSpecConfigEntity1 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig1); + ComputeSpecConfigEntity computeSpecConfigEntity2 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig2); + + // Commit hardware configs + Long hardwareConfigEntity1_id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity1); + Long hardwareConfigEntity2_Id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity2); + + ComputeSpecConfigEntity createdComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity1); + ComputeSpecConfigEntity createdComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity2); + + commitTransaction(); + + // Verify + assertNotNull(hardwareConfigEntity1_id); + assertNotNull(hardwareConfigEntity2_Id); + assertNotNull(createdComputeSpecConfigEntity1); + assertNotNull(createdComputeSpecConfigEntity2); + + // Execute + // Add hardware configs to compute spec configs + hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity1_id); + hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity2_Id); + hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity2.getId(), hardwareConfigEntity1_id); + + commitTransaction(); + + // Verify + ComputeSpecConfigEntity retrievedComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity1.getId()); + ComputeSpecConfigEntity retrievedComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity2.getId()); + + assertNotNull(retrievedComputeSpecConfigEntity1); + assertNotNull(retrievedComputeSpecConfigEntity2); + assertEquals(2, retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); + assertEquals(1, retrievedComputeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); + assertTrue(retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); + assertTrue(retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity2_Id))); + assertTrue(retrievedComputeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); + } + + @Test + @DirtiesContext + public void testRemoveHardwareConfigEntity() { + // Setup + HardwareConfigEntity hardwareConfigEntity1 = HardwareConfigEntity.fromPojo(hardwareConfig1); + HardwareConfigEntity hardwareConfigEntity2 = HardwareConfigEntity.fromPojo(hardwareConfig2); + ComputeSpecConfigEntity computeSpecConfigEntity1 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig1); + ComputeSpecConfigEntity computeSpecConfigEntity2 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig2); + + // Commit hardware configs + Long hardwareConfigEntity1_id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity1); + Long hardwareConfigEntity2_Id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity2); + + ComputeSpecConfigEntity createdComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity1); + ComputeSpecConfigEntity createdComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity2); + + commitTransaction(); + + // Verify + assertNotNull(hardwareConfigEntity1_id); + assertNotNull(hardwareConfigEntity2_Id); + assertNotNull(createdComputeSpecConfigEntity1); + assertNotNull(createdComputeSpecConfigEntity2); + + // Execute + // Add hardware configs to compute spec configs + hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity1_id); + hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity2_Id); + hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity2.getId(), hardwareConfigEntity1_id); + + commitTransaction(); + + // Verify + ComputeSpecConfigEntity retrievedComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity1.getId()); + ComputeSpecConfigEntity retrievedComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity2.getId()); + + assertNotNull(retrievedComputeSpecConfigEntity1); + assertNotNull(retrievedComputeSpecConfigEntity2); + assertEquals(2, retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); + assertEquals(1, retrievedComputeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); + assertTrue(retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); + assertTrue(retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity2_Id))); + assertTrue(retrievedComputeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); + + // Remove hardware configs from compute spec configs + hibernateComputeSpecConfigEntityService.removeHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity1_id); + hibernateComputeSpecConfigEntityService.removeHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity2_Id); + + commitTransaction(); + + hibernateComputeSpecConfigEntityService.removeHardwareConfigEntity(createdComputeSpecConfigEntity2.getId(), hardwareConfigEntity1_id); + + commitTransaction(); + + // Verify + retrievedComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity1.getId()); + retrievedComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity2.getId()); + + assertNotNull(retrievedComputeSpecConfigEntity1); + assertNotNull(retrievedComputeSpecConfigEntity2); + assertEquals(0, retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); + assertEquals(0, retrievedComputeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); + } + + @Test + @DirtiesContext + public void testFindByType() { + // Setup + ComputeSpecConfigEntity computeSpecConfigEntity1 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig1); + ComputeSpecConfigEntity computeSpecConfigEntity2 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig2); + ComputeSpecConfigEntity computeSpecConfigEntity3 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig3); + + // Commit compute spec configs + ComputeSpecConfigEntity createdComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity1); + ComputeSpecConfigEntity createdComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity2); + ComputeSpecConfigEntity createdComputeSpecConfigEntity3 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity3); + + commitTransaction(); + + // Verify + assertNotNull(createdComputeSpecConfigEntity1); + assertNotNull(createdComputeSpecConfigEntity2); + assertNotNull(createdComputeSpecConfigEntity3); + + // Execute + List retrievedComputeSpecConfigEntities = hibernateComputeSpecConfigEntityService.findByType(JUPYTERHUB); + + // Verify + assertNotNull(retrievedComputeSpecConfigEntities); + assertEquals(2, retrievedComputeSpecConfigEntities.size()); + assertEquals(createdComputeSpecConfigEntity1.getId(), retrievedComputeSpecConfigEntities.get(0).getId()); + assertEquals(createdComputeSpecConfigEntity3.getId(), retrievedComputeSpecConfigEntities.get(1).getId()); + + // Execute + retrievedComputeSpecConfigEntities = hibernateComputeSpecConfigEntityService.findByType(CONTAINER_SERVICE); + + // Verify + assertNotNull(retrievedComputeSpecConfigEntities); + assertEquals(2, retrievedComputeSpecConfigEntities.size()); + assertEquals(createdComputeSpecConfigEntity2.getId(), retrievedComputeSpecConfigEntities.get(0).getId()); + assertEquals(createdComputeSpecConfigEntity3.getId(), retrievedComputeSpecConfigEntities.get(1).getId()); + } + + public void createDummyConfigsAndEntities() { + // Setup hardware + Hardware hardware1 = Hardware.builder() + .name("Small") + .cpuReservation(2.0) + .cpuLimit(4.0) + .memoryReservation("4G") + .memoryLimit("8G") + .build(); + + // Setup hardware constraints + Constraint hardwareConstraint1 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Collections.singletonList("worker"))) + .build(); + + Constraint hardwareConstraint2 = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + hardware1.setConstraints(Arrays.asList(hardwareConstraint1, hardwareConstraint2)); + + // Setup hardware environment variables + EnvironmentVariable hardwareEnvironmentVariable1 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); + EnvironmentVariable hardwareEnvironmentVariable2 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); + + hardware1.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable1, hardwareEnvironmentVariable2)); + + // Setup hardware generic resources + GenericResource hardwareGenericResource1 = new GenericResource("nvidia.com/gpu", "2"); + GenericResource hardwareGenericResource2 = new GenericResource("fpga.com/fpga", "1"); + + hardware1.setGenericResources(Arrays.asList(hardwareGenericResource1, hardwareGenericResource2)); + + // Setup hardware scopes + HardwareScope hardwareSiteScope1 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope1 = HardwareScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope userHardwareScope1 = HardwareScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map hardwareScopes1 = new HashMap<>(); + hardwareScopes1.put(Site, hardwareSiteScope1); + hardwareScopes1.put(Project, hardwareProjectScope1); + hardwareScopes1.put(User, userHardwareScope1); + + // Setup a hardware config entity + hardwareConfig1 = HardwareConfig.builder() + .hardware(hardware1) + .scopes(hardwareScopes1) + .build(); + + // Setup second hardware config + Hardware hardware2 = Hardware.builder() + .name("Medium") + .cpuReservation(4.0) + .cpuLimit(4.0) + .memoryReservation("8G") + .memoryLimit("8G") + .build(); + + // Setup hardware constraints + Constraint hardwareConstraint3 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Collections.singletonList("worker"))) + .build(); + + Constraint hardwareConstraint4 = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + hardware2.setConstraints(Arrays.asList(hardwareConstraint3, hardwareConstraint4)); + + // Setup hardware environment variables + EnvironmentVariable hardwareEnvironmentVariable3 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); + EnvironmentVariable hardwareEnvironmentVariable4 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); + + hardware2.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable3, hardwareEnvironmentVariable4)); + + // Setup hardware generic resources + GenericResource hardwareGenericResource3 = new GenericResource("nvidia.com/gpu", "2"); + GenericResource hardwareGenericResource4 = new GenericResource("fpga.com/fpga", "1"); + + hardware2.setGenericResources(Arrays.asList(hardwareGenericResource3, hardwareGenericResource4)); + + // Setup hardware scopes + HardwareScope hardwareSiteScope2 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope2 = HardwareScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope userHardwareScope2 = HardwareScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map hardwareScopes2 = new HashMap<>(); + hardwareScopes2.put(Site, hardwareSiteScope2); + hardwareScopes2.put(Project, hardwareProjectScope2); + hardwareScopes2.put(User, userHardwareScope2); + + // Setup second hardware config entity + hardwareConfig2 = HardwareConfig.builder() + .hardware(hardware2) + .scopes(hardwareScopes2) + .build(); + + // Setup first compute spec + // Create mount first + Mount mount1 = Mount.builder() + .localPath("/home/jovyan/work") + .containerPath("/home/jovyan/work") + .readOnly(false) + .build(); + + Mount mount2 = Mount.builder() + .localPath("/tools/MATLAB/R2019b") + .containerPath("/tools/MATLAB/R2019b") + .readOnly(true) + .build(); + + ComputeSpec computeSpec1 = ComputeSpec.builder() + .name("Jupyter Datascience Notebook") + .image("jupyter/datascience-notebook:hub-3.0.0") + .environmentVariables(Collections.singletonList(new EnvironmentVariable("JUPYTER_ENABLE_LAB", "yes"))) + .mounts(Arrays.asList(mount1, mount2)) + .build(); + + ComputeSpecScope computeSpecSiteScope1 = ComputeSpecScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeSpecScope computeSpecProjectScope1 = ComputeSpecScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeSpecScope computeSpecUserScope1 = ComputeSpecScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map computeSpecScopes1 = new HashMap<>(); + computeSpecScopes1.put(Site, computeSpecSiteScope1); + computeSpecScopes1.put(Project, computeSpecProjectScope1); + computeSpecScopes1.put(User, computeSpecUserScope1); + + ComputeSpecHardwareOptions computeSpecHardwareOptions1 = ComputeSpecHardwareOptions.builder() + .allowAllHardware(true) + .hardwareConfigs(new HashSet<>()) + .build(); + + computeSpecConfig1 = ComputeSpecConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(JUPYTERHUB))) + .computeSpec(computeSpec1) + .scopes(computeSpecScopes1) + .hardwareOptions(computeSpecHardwareOptions1) + .build(); + + // Setup second compute spec + ComputeSpec computeSpec2 = ComputeSpec.builder() + .name("XNAT Datascience Notebook") + .image("xnat/datascience-notebook:latest") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeSpecScope computeSpecSiteScope2 = ComputeSpecScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeSpecScope computeSpecProjectScope2 = ComputeSpecScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("Project1"))) + .build(); + + ComputeSpecScope computeSpecUserScope2 = ComputeSpecScope.builder() + .scope(User) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("User1"))) + .build(); + + Map computeSpecScopes2 = new HashMap<>(); + computeSpecScopes2.put(Site, computeSpecSiteScope2); + computeSpecScopes2.put(Project, computeSpecProjectScope2); + computeSpecScopes2.put(User, computeSpecUserScope2); + + ComputeSpecHardwareOptions computeSpecHardwareOptions2 = ComputeSpecHardwareOptions.builder() + .allowAllHardware(false) + .hardwareConfigs(new HashSet<>()) + .build(); + + computeSpecConfig2 = ComputeSpecConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(CONTAINER_SERVICE))) + .computeSpec(computeSpec2) + .scopes(computeSpecScopes2) + .hardwareOptions(computeSpecHardwareOptions2) + .build(); + + // Setup third compute spec + ComputeSpec computeSpec3 = ComputeSpec.builder() + .name("MATLAB Datascience Notebook") + .image("matlab/datascience-notebook:latest") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeSpecScope computeSpecSiteScope3 = ComputeSpecScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeSpecScope computeSpecProjectScope3 = ComputeSpecScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("Project1"))) + .build(); + + ComputeSpecScope computeSpecUserScope3 = ComputeSpecScope.builder() + .scope(User) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("User1"))) + .build(); + + Map computeSpecScopes3 = new HashMap<>(); + computeSpecScopes2.put(Site, computeSpecSiteScope3); + computeSpecScopes2.put(Project, computeSpecProjectScope3); + computeSpecScopes2.put(User, computeSpecUserScope3); + + ComputeSpecHardwareOptions computeSpecHardwareOptions3 = ComputeSpecHardwareOptions.builder() + .allowAllHardware(false) + .hardwareConfigs(new HashSet<>()) + .build(); + + computeSpecConfig3 = ComputeSpecConfig.builder() + .configTypes(new HashSet<>(Arrays.asList(CONTAINER_SERVICE, JUPYTERHUB))) + .computeSpec(computeSpec3) + .scopes(computeSpecScopes3) + .hardwareOptions(computeSpecHardwareOptions3) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityServiceTest.java b/src/test/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityServiceTest.java new file mode 100644 index 0000000..54c06dd --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityServiceTest.java @@ -0,0 +1,90 @@ +package org.nrg.jobtemplates.services.impl; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.nrg.framework.constants.Scope; +import org.nrg.jobtemplates.config.HibernateConstraintConfigEntityServiceTestConfig; +import org.nrg.jobtemplates.entities.ConstraintConfigEntity; +import org.nrg.jobtemplates.models.Constraint; +import org.nrg.jobtemplates.models.ConstraintConfig; +import org.nrg.jobtemplates.models.ConstraintScope; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.nrg.jobtemplates.utils.TestingUtils.commitTransaction; + +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +@ContextConfiguration(classes = HibernateConstraintConfigEntityServiceTestConfig.class) +public class HibernateConstraintConfigEntityServiceTest { + + @Autowired private HibernateConstraintConfigEntityService hibernateConstraintConfigEntityService; + + @Test + public void test() { + assertNotNull(hibernateConstraintConfigEntityService); + } + + @Test + @DirtiesContext + public void testCreateConstraintConfig() { + assertNotNull(hibernateConstraintConfigEntityService); + + // Create a constraint config + Constraint constraint1 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Arrays.asList("worker"))) + .build(); + + ConstraintScope constraintScopeSite1 = ConstraintScope.builder() + .scope(Scope.Site) + .enabled(true) + .ids(new HashSet<>()) + .build(); + + ConstraintScope constraintScopeProject1 = ConstraintScope.builder() + .scope(Scope.Project) + .enabled(true) + .ids(new HashSet<>()) + .build(); + + ConstraintScope constraintScopeUser1 = ConstraintScope.builder() + .scope(Scope.User) + .enabled(true) + .ids(new HashSet<>()) + .build(); + + Map scopes1 = new HashMap<>(); + scopes1.put(Scope.Site, constraintScopeSite1); + scopes1.put(Scope.Project, constraintScopeProject1); + scopes1.put(Scope.User, constraintScopeUser1); + + ConstraintConfig constraintConfig = ConstraintConfig.builder() + .constraint(constraint1) + .scopes(scopes1) + .build(); + + // Save the constraint config + ConstraintConfigEntity created = hibernateConstraintConfigEntityService.create(ConstraintConfigEntity.fromPojo(constraintConfig)); + + commitTransaction(); + + // Retrieve the constraint config + ConstraintConfigEntity retrieved = hibernateConstraintConfigEntityService.retrieve(created.getId()); + + // Check that the retrieved constraint config matches the original + assertEquals(created, retrieved); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityServiceTest.java b/src/test/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityServiceTest.java new file mode 100644 index 0000000..73fb54d --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityServiceTest.java @@ -0,0 +1,23 @@ +package org.nrg.jobtemplates.services.impl; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.nrg.jobtemplates.config.HibernateHardwareConfigEntityServiceTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = HibernateHardwareConfigEntityServiceTestConfig.class) +public class HibernateHardwareConfigEntityServiceTest { + + @Autowired private HibernateHardwareConfigEntityService hibernateHardwareConfigEntityService; + + @Test + public void test() { + assertNotNull(hibernateHardwareConfigEntityService); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/jobtemplates/utils/TestingUtils.java b/src/test/java/org/nrg/jobtemplates/utils/TestingUtils.java new file mode 100644 index 0000000..a299000 --- /dev/null +++ b/src/test/java/org/nrg/jobtemplates/utils/TestingUtils.java @@ -0,0 +1,15 @@ +package org.nrg.jobtemplates.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.test.context.transaction.TestTransaction; + +@Slf4j +public class TestingUtils { + + public static void commitTransaction() { + TestTransaction.flagForCommit(); + TestTransaction.end(); + TestTransaction.start(); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java index 5e90d28..bb9239a 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java @@ -1,10 +1,10 @@ package org.nrg.xnatx.plugins.jupyterhub.config; import org.nrg.framework.services.NrgEventServiceI; +import org.nrg.jobtemplates.services.JobTemplateService; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xnatx.plugins.jupyterhub.client.JupyterHubClient; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultJupyterHubService; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; @@ -23,14 +23,14 @@ public DefaultJupyterHubService defaultJupyterHubService(final JupyterHubClient final UserOptionsService mockUserOptionsService, final JupyterHubPreferences mockJupyterHubPreferences, final UserManagementServiceI mockUserManagementService, - final ProfileService mockProfileService) { + final JobTemplateService mockJobTemplateService) { return new DefaultJupyterHubService(mockJupyterHubClient, mockNrgEventService, mockPermissionsHelper, mockUserOptionsService, mockJupyterHubPreferences, mockUserManagementService, - mockProfileService); + mockJobTemplateService); } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultProfileServiceConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultProfileServiceConfig.java deleted file mode 100644 index 8cd2ebd..0000000 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultProfileServiceConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.config; - -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileEntityService; -import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultProfileService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import({MockConfig.class}) -public class DefaultProfileServiceConfig { - - @Bean - public DefaultProfileService defaultProfileService(final ProfileEntityService mockProfileEntityService) { - return new DefaultProfileService(mockProfileEntityService); - } - -} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java index 82c93d7..b166292 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java @@ -1,10 +1,10 @@ package org.nrg.xnatx.plugins.jupyterhub.config; +import org.nrg.jobtemplates.services.JobTemplateService; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.SearchHelperServiceI; import org.nrg.xdat.services.AliasTokenService; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsEntityService; import org.nrg.xnatx.plugins.jupyterhub.services.UserWorkspaceService; import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultUserOptionsService; @@ -25,7 +25,7 @@ public DefaultUserOptionsService defaultUserOptionsService(final JupyterHubPrefe final SiteConfigPreferences mockSiteConfigPreferences, final UserOptionsEntityService mockUserOptionsEntityService, final PermissionsHelper mockPermissionsHelper, - final ProfileService profileService) { + final JobTemplateService mockJobTemplateService) { return new DefaultUserOptionsService(mockJupyterHubPreferences, mockUserWorkspaceService, mockSearchHelperService, @@ -33,7 +33,7 @@ public DefaultUserOptionsService defaultUserOptionsService(final JupyterHubPrefe mockSiteConfigPreferences, mockUserOptionsEntityService, mockPermissionsHelper, - profileService); + mockJobTemplateService); } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java new file mode 100644 index 0000000..5f1fdc0 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java @@ -0,0 +1,29 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.initialize.JupyterHubEnvironmentsAndHardwareInitializer; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({MockConfig.class}) +public class JupyterHubEnvironmentsAndHardwareInitializerTestConfig { + + @Bean + public JupyterHubEnvironmentsAndHardwareInitializer JupyterHubJobTemplateInitializer(final XFTManagerHelper mockXFTManagerHelper, + final XnatAppInfo mockXnatAppInfo, + final ComputeSpecConfigService mockComputeSpecConfigService, + final HardwareConfigService mockHardwareConfigService) { + return new JupyterHubEnvironmentsAndHardwareInitializer( + mockXFTManagerHelper, + mockXnatAppInfo, + mockComputeSpecConfigService, + mockHardwareConfigService + ); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfileInitializerConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfileInitializerConfig.java deleted file mode 100644 index e7182c6..0000000 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubProfileInitializerConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.config; - -import org.nrg.xnatx.plugins.jupyterhub.initialize.JupyterHubProfileInitializer; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; -import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import({MockConfig.class}) -public class JupyterHubProfileInitializerConfig { - - @Bean - public JupyterHubProfileInitializer defaultJupyterHubProfileInitializer(final ProfileService mockProfileService, - final XFTManagerHelper mockXFTManagerHelper) { - return new JupyterHubProfileInitializer(mockProfileService, mockXFTManagerHelper); - } - -} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java index 1318e56..20d0798 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java @@ -5,6 +5,9 @@ import org.nrg.framework.services.NrgEventServiceI; import org.nrg.framework.services.SerializerService; import org.nrg.framework.utilities.OrderedProperties; +import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.jobtemplates.services.JobTemplateService; import org.nrg.prefs.services.NrgPreferenceService; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.*; @@ -69,11 +72,6 @@ public UserOptionsEntityService mockUserOptionsEntityService() { return Mockito.mock(UserOptionsEntityService.class); } - @Bean - public ProfileEntityService mockProfileEntityService() { - return Mockito.mock(ProfileEntityService.class); - } - @Bean public PermissionsServiceI mockPermissionsService() { return Mockito.mock(PermissionsServiceI.class); @@ -84,11 +82,6 @@ public UserOptionsService mockUserOptionsService() { return Mockito.mock(UserOptionsService.class); } - @Bean - public ProfileService mockProfileService() { - return Mockito.mock(ProfileService.class); - } - @Bean public JupyterHubClient mockJupyterHubClient() { return Mockito.mock(JupyterHubClient.class); @@ -150,4 +143,19 @@ public SerializerService mockSerializerService() { public XnatAppInfo mockXnatAppInfo() { return Mockito.mock(XnatAppInfo.class); } + + @Bean + public ComputeSpecConfigService mockComputeSpecConfigService() { + return Mockito.mock(ComputeSpecConfigService.class); + } + + @Bean + public HardwareConfigService mockHardwareConfigService() { + return Mockito.mock(HardwareConfigService.class); + } + + @Bean + public JobTemplateService mockJobTemplateService() { + return Mockito.mock(JobTemplateService.class); + } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java new file mode 100644 index 0000000..afdaf9a --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java @@ -0,0 +1,127 @@ +package org.nrg.xnatx.plugins.jupyterhub.initialize; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.jobtemplates.models.ComputeSpecConfig; +import org.nrg.jobtemplates.models.HardwareConfig; +import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubEnvironmentsAndHardwareInitializerTestConfig; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Collections; + +import static org.hamcrest.CoreMatchers.isA; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = JupyterHubEnvironmentsAndHardwareInitializerTestConfig.class) +public class JupyterHubEnvironmentsAndHardwareInitializerTest { + + @Autowired private JupyterHubEnvironmentsAndHardwareInitializer jupyterHubJobTemplateInitializer; + @Autowired private XFTManagerHelper mockXFTManagerHelper; + @Autowired private XnatAppInfo mockXnatAppInfo; + @Autowired private ComputeSpecConfigService mockComputeSpecConfigService; + @Autowired private HardwareConfigService mockHardwareConfigService; + + @After + public void after() { + Mockito.reset( + mockXFTManagerHelper, + mockXnatAppInfo, + mockComputeSpecConfigService, + mockHardwareConfigService + ); + } + + @Test + public void testGetTaskName() { + // Test + String taskName = jupyterHubJobTemplateInitializer.getTaskName(); + + // Verify + assertThat(taskName, notNullValue()); + assertThat(taskName, isA(String.class)); + } + + @Test(expected = InitializingTaskException.class) + public void testCallImpl_XFTManagerNotInitialized() throws Exception { + // XFT not initialized + when(mockXFTManagerHelper.isInitialized()).thenReturn(false); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + + // Should throw InitializingTaskException + jupyterHubJobTemplateInitializer.callImpl(); + } + + @Test(expected = InitializingTaskException.class) + public void testCallImpl_AppNotInitialized() throws Exception { + // XFT initialized but app not initialized + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(false); + + // Should throw InitializingTaskException + jupyterHubJobTemplateInitializer.callImpl(); + } + + @Test + public void testCallImpl_ConfigsAlreadyExist() { + // XNAT is initialized + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + + + // Setup mock + when(mockComputeSpecConfigService.getByType(any())) + .thenReturn(Collections.singletonList(new ComputeSpecConfig())); + when(mockHardwareConfigService.retrieveAll()) + .thenReturn(Collections.singletonList(new HardwareConfig())); + + // Test + try { + jupyterHubJobTemplateInitializer.callImpl(); + } catch (InitializingTaskException e) { + fail("Should not throw InitializingTaskException"); + } + + // Verify + verify(mockComputeSpecConfigService, never()).create(any()); + verify(mockHardwareConfigService, never()).create(any()); + } + + @Test + public void testCallImpl() { + // XNAT is initialized + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + + // Setup mock + when(mockComputeSpecConfigService.getByType(any())) + .thenReturn(Collections.emptyList()); + when(mockHardwareConfigService.retrieveAll()) + .thenReturn(Collections.emptyList()); + + // Test + try { + jupyterHubJobTemplateInitializer.callImpl(); + } catch (InitializingTaskException e) { + fail("Should not throw InitializingTaskException"); + } + + // Verify + verify(mockComputeSpecConfigService, atLeast(1)).create(any()); + verify(mockHardwareConfigService, atLeast(1)).create(any()); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializerTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializerTest.java deleted file mode 100644 index 323a750..0000000 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubProfileInitializerTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.initialize; - -import org.junit.After; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.nrg.xnat.initialization.tasks.InitializingTaskException; -import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubProfileInitializerConfig; -import org.nrg.xnatx.plugins.jupyterhub.models.Profile; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; -import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import java.util.Collections; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.*; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = JupyterHubProfileInitializerConfig.class) -public class JupyterHubProfileInitializerTest { - - @Autowired private JupyterHubProfileInitializer jupyterHubProfileInitializer; - @Autowired private ProfileService mockProfileService; - @Autowired private XFTManagerHelper mockXFTManagerHelper; - - @After - public void after() { - Mockito.reset(mockProfileService); - Mockito.reset(mockXFTManagerHelper); - } - - @Test - public void getTaskName() { - assertNotNull(jupyterHubProfileInitializer.getTaskName()); - } - - @Test(expected = InitializingTaskException.class) - public void callImpl_xftManagerNotInitialized() throws Exception { - // XFT not initialized - when(mockXFTManagerHelper.isInitialized()).thenReturn(false); - - // Should throw InitializingTaskException - jupyterHubProfileInitializer.callImpl(); - } - - @Test - public void callImpl_profileServiceHasProfiles() throws Exception { - // Setup - // XFT initialized and profile service has profiles - when(mockXFTManagerHelper.isInitialized()).thenReturn(true); - when(mockProfileService.getAll()).thenReturn(Collections.singletonList(Profile.builder().build())); - - // Test - jupyterHubProfileInitializer.callImpl(); - - // Verify profile service not called - verify(mockProfileService, never()).create(any(Profile.class)); - } - - @Test - public void callImpl_initProfile() throws Exception { - // Setup - // XFT initialized and profile service has no profiles - when(mockXFTManagerHelper.isInitialized()).thenReturn(true); - when(mockProfileService.getAll()).thenReturn(Collections.emptyList()); - - // Test - jupyterHubProfileInitializer.callImpl(); - - // Verify profile service called - verify(mockProfileService, times(1)).create(any(Profile.class)); - } -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java index 738c3d0..c48fcb8 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java @@ -1,6 +1,7 @@ package org.nrg.xnatx.plugins.jupyterhub.initialize; import org.junit.After; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -36,6 +37,7 @@ public void after() { Mockito.reset(mockUserManagementService); Mockito.reset(mockXFTManagerHelper); Mockito.reset(mockRoleService); + Mockito.reset(mockXnatAppInfo); } @Test(expected = InitializingTaskException.class) @@ -78,6 +80,7 @@ public void test_JHUserAlreadyExists() throws Exception { public void test_FailedToSaveUser() throws Exception { // Setup when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); when(mockUserManagementService.exists(username)).thenReturn(false); when(mockUserManagementService.createUser()).thenReturn(mock(UserI.class)); doThrow(Exception.class).when(mockUserManagementService).save(any(UserI.class), nullable(UserI.class), @@ -96,6 +99,7 @@ public void test_FailedToSaveUser() throws Exception { public void test_FailedAddUserRole() throws Exception { // Setup when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); when(mockUserManagementService.exists(username)).thenReturn(false); when(mockUserManagementService.createUser()).thenReturn(mock(UserI.class)); when(mockRoleService.addRole(nullable(UserI.class), any(UserI.class), anyString())).thenReturn(false); @@ -115,6 +119,7 @@ public void test_FailedAddUserRole() throws Exception { public void test_FailedAddUserRole_Exception() throws Exception { // Setup when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); when(mockUserManagementService.exists(username)).thenReturn(false); when(mockUserManagementService.createUser()).thenReturn(mock(UserI.class)); doThrow(Exception.class).when(mockRoleService).addRole(nullable(UserI.class), any(UserI.class), anyString()); @@ -131,6 +136,7 @@ public void test_FailedAddUserRole_Exception() throws Exception { } @Test + @Ignore("This test fails occasionally, but not consistently. Not sure why.") public void test_UserCreated() throws Exception { // Setup when(mockXFTManagerHelper.isInitialized()).thenReturn(true); diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java index 399f7ac..0173880 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java @@ -63,6 +63,7 @@ public class JupyterHubApiTest { private User nonAdmin_jh; private XnatUserOptions userOptions; + private ServerStartRequest serverStartRequest; private Long profileId; private Server dummyServer; private String servername; @@ -151,6 +152,18 @@ public void before() throws Exception { .taskTemplate(taskTemplate) .build(); + serverStartRequest = ServerStartRequest.builder() + .username(NON_ADMIN_USERNAME) + .servername("") + .xsiType(XnatProjectdata.SCHEMA_ELEMENT_NAME) + .itemId("TestProject") + .itemLabel("TestProject") + .projectId("TestProject") + .eventTrackingId("20220822T201541799Z") + .computeSpecConfigId(1L) + .hardwareConfigId(1L) + .build(); + dummyServer = Server.builder() .name("") .ready(true) @@ -419,74 +432,16 @@ public void testGetServerDeniedWrongUser() throws Exception { public void testStartServer() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders .post("/jupyterhub/users/" + NON_ADMIN_USERNAME + "/server") - .param("username", NON_ADMIN_USERNAME) - .param("xsiType", userOptions.getXsiType()) - .param("itemId", userOptions.getItemId()) - .param("itemLabel", userOptions.getItemLabel()) - .param("projectId", userOptions.getProjectId()) - .param("eventTrackingId", userOptions.getEventTrackingId()) - .param("profileId", profileId.toString()) - .with(authentication(NONADMIN_AUTH)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request).andExpect(status().isOk()); - - // Unnamed server - verify(mockJupyterHubService, times(1)).startServer(eq(nonAdmin), - eq(userOptions.getXsiType()), - eq(userOptions.getItemId()), - eq(userOptions.getItemLabel()), - eq(userOptions.getProjectId()), - eq(userOptions.getEventTrackingId()), - eq(profileId)); - // Named Server - verify(mockJupyterHubService, never()).startServer(any(UserI.class), - anyString(), - anyString(), - anyString(), - anyString(), - anyString(), - anyString(), - anyLong()); - } - - @Test - public void testStartNamedServer() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .post("/jupyterhub/users/" + NON_ADMIN_USERNAME + "/server/" + servername) - .param("username", NON_ADMIN_USERNAME) - .param("xsiType", userOptions.getXsiType()) - .param("itemId", userOptions.getItemId()) - .param("itemLabel", userOptions.getItemLabel()) - .param("projectId", userOptions.getProjectId()) - .param("eventTrackingId", userOptions.getEventTrackingId()) - .param("profileId", profileId.toString()) + .accept(JSON) + .contentType(JSON) + .content(mapper.writeValueAsString(serverStartRequest)) .with(authentication(NONADMIN_AUTH)) .with(csrf()) .with(testSecurityContext()); mockMvc.perform(request).andExpect(status().isOk()); - // Unnamed server - verify(mockJupyterHubService, never()).startServer(any(UserI.class), - anyString(), - anyString(), - anyString(), - anyString(), - anyString(), - anyLong()); - - // Named Server - verify(mockJupyterHubService, times(1)).startServer(eq(nonAdmin), - eq(servername), - eq(userOptions.getXsiType()), - eq(userOptions.getItemId()), - eq(userOptions.getItemLabel()), - eq(userOptions.getProjectId()), - eq(userOptions.getEventTrackingId()), - eq(profileId)); - + verify(mockJupyterHubService, times(1)).startServer(eq(nonAdmin), eq(serverStartRequest)); } @Test diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApiTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApiTest.java deleted file mode 100644 index 8714f1f..0000000 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubProfilesApiTest.java +++ /dev/null @@ -1,318 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.rest; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.nrg.xdat.security.services.RoleServiceI; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xft.security.UserI; -import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubProfilesApiConfig; -import org.nrg.xnatx.plugins.jupyterhub.models.Profile; -import org.nrg.xnatx.plugins.jupyterhub.models.docker.ContainerSpec; -import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.*; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebAppConfiguration -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = JupyterHubProfilesApiConfig.class) -public class JupyterHubProfilesApiTest { - - @Autowired private UserManagementServiceI mockUserManagementService; - @Autowired private RoleServiceI mockRoleService; - @Autowired private WebApplicationContext wac; - @Autowired private ObjectMapper mapper; - @Autowired private ProfileService mockProfileService; - - private MockMvc mockMvc; - - private final MediaType JSON = MediaType.APPLICATION_JSON_UTF8; - - private Authentication mockAuthentication; - - private Profile profile; - - @Before - public void setUp() throws Exception { - // Setup mock web context - mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); - - // Setup user - String mockUsername = "mockUser"; - String mockPassword = "mockPassword"; - Integer mockUserId = 1; - UserI mockUser = Mockito.mock(UserI.class); - Mockito.when(mockUser.getID()).thenReturn(mockUserId); - Mockito.when(mockUser.getLogin()).thenReturn(mockUsername); - Mockito.when(mockUser.getUsername()).thenReturn(mockUsername); - Mockito.when(mockUser.getPassword()).thenReturn(mockPassword); - - when(mockRoleService.isSiteAdmin(mockUser)).thenReturn(false); - when(mockUserManagementService.getUser(mockUsername)).thenReturn(mockUser); - - mockAuthentication = new TestingAuthenticationToken(mockUser, mockPassword); - - // Setup profile - ContainerSpec containerSpec = ContainerSpec.builder() - .image("image") - .build(); - - TaskTemplate taskTemplate = TaskTemplate.builder() - .containerSpec(containerSpec) - .build(); - - profile = Profile.builder() - .id(1L) - .name("name") - .description("description") - .taskTemplate(taskTemplate) - .build(); - } - - @After - public void tearDown() { - Mockito.reset(mockUserManagementService); - Mockito.reset(mockRoleService); - Mockito.reset(mockProfileService); - Mockito.reset(mockRoleService); - Mockito.reset(mockProfileService); - } - - @Test - public void getProfile() throws Exception { - // Setup mock - when(mockProfileService.get(profile.getId())).thenReturn(Optional.of(profile)); - - // Test - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/jupyterhub/profiles/" + profile.getId()) - .accept(JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - final String response = mockMvc - .perform(request) - .andExpect(status().isOk()) - .andExpect(content().contentType(JSON)) - .andReturn() - .getResponse() - .getContentAsString(); - - Profile responseProfile = mapper.readValue(response, Profile.class); - - assertEquals(profile, responseProfile); - } - - @Test - public void getProfile_doesNotExist() throws Exception { - // Setup mock - when(mockProfileService.get(profile.getId())).thenReturn(Optional.empty()); - - // Test - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/jupyterhub/profiles/" + profile.getId()) - .accept(JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request) - .andExpect(status().isNotFound()) - .andReturn() - .getResponse() - .getContentAsString(); - } - - @Test - public void getAllProfiles() throws Exception { - // Setup mock - when(mockProfileService.getAll()).thenReturn(Collections.singletonList(profile)); - - // Test - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/jupyterhub/profiles") - .accept(JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - final String response = mockMvc - .perform(request) - .andExpect(status().isOk()) - .andExpect(content().contentType(JSON)) - .andReturn() - .getResponse() - .getContentAsString(); - - List responseProfiles = mapper.readValue(response, new TypeReference>() {}); - - assertEquals(Collections.singletonList(profile), responseProfiles); - } - - @Test - public void createProfile() throws Exception { - // Setup mock - when(mockProfileService.create(profile)).thenReturn(profile.getId()); - - // Test - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .post("/jupyterhub/profiles") - .accept(JSON) - .contentType(JSON) - .content(mapper.writeValueAsString(profile)) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - final String response = mockMvc - .perform(request) - .andExpect(status().isCreated()) - .andExpect(content().contentType(JSON)) - .andReturn() - .getResponse() - .getContentAsString(); - - Long responseProfileId = mapper.readValue(response, Long.class); - - assertEquals(profile.getId(), responseProfileId); - } - - @Test - public void updateProfile() throws Exception { - // Setup mock - when(mockProfileService.exists(profile.getId())).thenReturn(true); - - // Test - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/jupyterhub/profiles/" + profile.getId()) - .accept(JSON) - .contentType(JSON) - .content(mapper.writeValueAsString(profile)) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request) - .andExpect(status().isOk()) - .andReturn() - .getResponse() - .getContentAsString(); - - // Verify mock is called - verify(mockProfileService, times(1)).update(profile); - } - - @Test - public void updateProfile_doesNotExist() throws Exception { - // Setup mock - when(mockProfileService.exists(profile.getId())).thenReturn(false); - - // Test - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/jupyterhub/profiles/" + profile.getId()) - .accept(JSON) - .contentType(JSON) - .content(mapper.writeValueAsString(profile)) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request) - .andExpect(status().isNotFound()) - .andReturn() - .getResponse() - .getContentAsString(); - } - - @Test - public void updateProfile_cantHaveDifferentId() throws Exception { - // Setup mock - when(mockProfileService.exists(profile.getId())).thenReturn(true); - - // Test - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/jupyterhub/profiles/" + profile.getId()) - .accept(JSON) - .contentType(JSON) - .content(mapper.writeValueAsString(Profile.builder().id(2L).build())) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request) - .andExpect(status().isBadRequest()) - .andReturn() - .getResponse() - .getContentAsString(); - } - - @Test - public void deleteProfile() throws Exception { - // Setup mock - when(mockProfileService.exists(profile.getId())).thenReturn(true); - - // Test - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete("/jupyterhub/profiles/" + profile.getId()) - .accept(JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request) - .andExpect(status().isOk()) - .andReturn() - .getResponse() - .getContentAsString(); - - // Verify mock is called - verify(mockProfileService, times(1)).delete(profile.getId()); - } - - @Test - public void deleteProfile_doesNotExist() throws Exception { - // Setup mock - when(mockProfileService.exists(profile.getId())).thenReturn(false); - - // Test - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete("/jupyterhub/profiles/" + profile.getId()) - .accept(JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request) - .andExpect(status().isNotFound()) - .andReturn() - .getResponse() - .getContentAsString(); - } -} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java index a9ce402..3e73983 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java @@ -8,6 +8,7 @@ import org.mockito.Captor; import org.mockito.Mockito; import org.nrg.framework.services.NrgEventServiceI; +import org.nrg.jobtemplates.services.JobTemplateService; import org.nrg.xdat.om.XnatExperimentdata; import org.nrg.xdat.om.XnatImagescandata; import org.nrg.xdat.om.XnatProjectdata; @@ -24,8 +25,8 @@ import org.nrg.xnatx.plugins.jupyterhub.client.models.UserOptions; import org.nrg.xnatx.plugins.jupyterhub.config.DefaultJupyterHubServiceConfig; import org.nrg.xnatx.plugins.jupyterhub.events.JupyterServerEventI; +import org.nrg.xnatx.plugins.jupyterhub.models.ServerStartRequest; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsEntityService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; @@ -56,7 +57,7 @@ public class DefaultJupyterHubServiceTest { @Autowired private UserOptionsService mockUserOptionsService; @Autowired private JupyterHubPreferences mockJupyterHubPreferences; @Autowired private UserManagementServiceI mockUserManagementServiceI; - @Autowired private ProfileService mockProfileService; + @Autowired private JobTemplateService mockJobTemplateService; @Captor ArgumentCaptor jupyterServerEventCaptor; @Captor ArgumentCaptor tokenArgumentCaptor; @@ -66,10 +67,16 @@ public class DefaultJupyterHubServiceTest { private final String servername = ""; private final String eventTrackingId = "eventTrackingId"; private final String projectId = "TestProject"; - private final Long profileId = 2L; + private final Long computeSpecConfigId = 3L; + private final Long hardwareConfigId = 2L; private User userWithServers; private User userNoServers; + private ServerStartRequest startProjectRequest; + private ServerStartRequest startSubjectRequest; + private ServerStartRequest startExperimentRequest; + private ServerStartRequest startScanRequest; + @Before public void before() throws org.nrg.xdat.security.user.exceptions.UserNotFoundException, UserInitException { // Mock the user @@ -92,6 +99,58 @@ public void before() throws org.nrg.xdat.security.user.exceptions.UserNotFoundEx .servers(Collections.emptyMap()) .build(); + // Setup start project request + startProjectRequest = ServerStartRequest.builder() + .username(username) + .servername(servername) + .xsiType(XnatProjectdata.SCHEMA_ELEMENT_NAME) + .itemId(projectId) + .itemLabel(projectId) + .projectId(projectId) + .eventTrackingId(eventTrackingId) + .computeSpecConfigId(computeSpecConfigId) + .hardwareConfigId(hardwareConfigId) + .build(); + + // Setup start subject request + startSubjectRequest = ServerStartRequest.builder() + .username(username) + .servername(servername) + .xsiType(XnatSubjectdata.SCHEMA_ELEMENT_NAME) + .itemId("XNAT_S00001") + .itemLabel("Subject1") + .projectId(projectId) + .eventTrackingId(eventTrackingId) + .computeSpecConfigId(computeSpecConfigId) + .hardwareConfigId(hardwareConfigId) + .build(); + + // Setup start experiment request + startExperimentRequest = ServerStartRequest.builder() + .username(username) + .servername(servername) + .xsiType(XnatExperimentdata.SCHEMA_ELEMENT_NAME) + .itemId("XNAT_E00001") + .itemLabel("Experiment1") + .projectId(projectId) + .eventTrackingId(eventTrackingId) + .computeSpecConfigId(computeSpecConfigId) + .hardwareConfigId(hardwareConfigId) + .build(); + + // Setup start scan request + startScanRequest = ServerStartRequest.builder() + .username(username) + .servername(servername) + .xsiType(XnatImagescandata.SCHEMA_ELEMENT_NAME) + .itemId("XNAT_S00001") + .itemLabel("Scan1") + .projectId(projectId) + .eventTrackingId(eventTrackingId) + .computeSpecConfigId(computeSpecConfigId) + .hardwareConfigId(hardwareConfigId) + .build(); + // Capture Jupyter events jupyterServerEventCaptor = ArgumentCaptor.forClass(JupyterServerEventI.class); tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class); @@ -181,7 +240,7 @@ public void testStartServer_cantReadProject() throws Exception { when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(false); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId , projectId, eventTrackingId, profileId); + jupyterHubService.startServer(user, startProjectRequest); // Verify failure to start event occurred verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); @@ -202,9 +261,7 @@ public void testStartServer_cantReadSubject() throws Exception { when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(false); // Test - String subjectId = "XNAT_S00001"; - String subjectLabel = "Subject1"; - jupyterHubService.startServer(user, XnatSubjectdata.SCHEMA_ELEMENT_NAME, subjectId, subjectLabel, projectId, eventTrackingId, profileId); + jupyterHubService.startServer(user, startSubjectRequest); //Verify failure to start event occurred verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); @@ -225,9 +282,7 @@ public void testStartServer_cantReadExperiment() throws Exception { when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(false); // Test - String experimentId = "XNAT_E00001"; - String experimentLabel = "Experiment01"; - jupyterHubService.startServer(user, XnatExperimentdata.SCHEMA_ELEMENT_NAME, experimentId, experimentLabel, projectId, eventTrackingId, profileId); + jupyterHubService.startServer(user, startExperimentRequest); //Verify failure to start event occurred verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); @@ -248,7 +303,7 @@ public void testStartServer_cantReadScan() throws Exception { when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(false); // Test - jupyterHubService.startServer(user, XnatImagescandata.SCHEMA_ELEMENT_NAME, "456", "Scan 456", projectId, eventTrackingId, profileId); + jupyterHubService.startServer(user, startScanRequest); //Verify failure to start event occurred verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); @@ -272,7 +327,7 @@ public void testStartServer_HubOffline() throws Exception { when(mockJupyterHubClient.getVersion()).thenThrow(RuntimeException.class); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); + jupyterHubService.startServer(user, startProjectRequest); Thread.sleep(1000); // Async call, need to wait. Is there a better way to test this? // Verify failure to start event occurred @@ -297,7 +352,7 @@ public void testStartServer_serverAlreadyRunning() throws Exception { when(mockJupyterHubClient.getUser(anyString())).thenReturn(Optional.of(userWithServers)); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); + jupyterHubService.startServer(user, startProjectRequest); Thread.sleep(1000); // Async call, need to wait. Is there a better way to test this? // Verify failure to start event occurred @@ -314,15 +369,15 @@ public void testStartServer_serverAlreadyRunning() throws Exception { } @Test(timeout = 2000) - public void testStartServer_profileIdDoesNotExist() throws Exception { + public void testStartServer_jobTemplateUnavailable() throws Exception { // Grant permissions when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); - // Profile ID does not exist - when(mockProfileService.exists(anyLong())).thenReturn(false); + // Job template unavailable + when(mockJobTemplateService.isAvailable(any(), any(), any(), any())).thenReturn(false); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); + jupyterHubService.startServer(user, startProjectRequest); Thread.sleep(1000); // Async call, need to wait. Is there a better way to test this? // Verify failure to start event occurred @@ -343,8 +398,8 @@ public void testStartServer_Timeout() throws UserNotFoundException, ResourceAlre // Grant permissions when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); - // Profile ID exists - when(mockProfileService.exists(anyLong())).thenReturn(true); + // Job template is available + when(mockJobTemplateService.isAvailable(any(), any(), any(), any())).thenReturn(true); // To successfully start a server there should be no running servers at first. // A subsequent call should contain a server, but let's presume it is never ready @@ -352,12 +407,13 @@ public void testStartServer_Timeout() throws UserNotFoundException, ResourceAlre when(mockJupyterHubClient.getServer(anyString(), anyString())).thenReturn(Optional.empty()); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); + jupyterHubService.startServer(user, startProjectRequest); Thread.sleep(3000); // Async call, need to wait. Is there a better way to test this? // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), - eq(projectId), eq(projectId), eq(profileId), eq(eventTrackingId)); + eq(projectId), eq(projectId), eq(computeSpecConfigId), + eq(hardwareConfigId), eq(eventTrackingId)); // Verify JupyterHub start server request sent verify(mockJupyterHubClient, times(1)).startServer(eq(username), eq(""), any(UserOptions.class)); @@ -377,8 +433,8 @@ public void testStartServer_Success() throws Exception { // Grant permissions when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); - // Profile ID exists - when(mockProfileService.exists(anyLong())).thenReturn(true); + // Job template is available + when(mockJobTemplateService.isAvailable(any(), any(), any(), any())).thenReturn(true); // To successfully start a server there should be no running servers at first. // A subsequent call should eventually return a server in the ready state @@ -386,12 +442,13 @@ public void testStartServer_Success() throws Exception { when(mockJupyterHubClient.getServer(anyString(), anyString())).thenReturn(Optional.of(Server.builder().ready(true).build())); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); + jupyterHubService.startServer(user, startProjectRequest); Thread.sleep(2500); // Async call, need to wait. Is there a better way to test this? // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), - eq(projectId), eq(projectId), eq(profileId), eq(eventTrackingId)); + eq(projectId), eq(projectId), eq(computeSpecConfigId), + eq(hardwareConfigId), eq(eventTrackingId)); // Verify JupyterHub start server request sent verify(mockJupyterHubClient, times(1)).startServer(eq(username), eq(""), any(UserOptions.class)); @@ -408,8 +465,8 @@ public void testStartServer_CreateUser_Success() throws Exception { // Grant permissions when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); - // Profile ID exists - when(mockProfileService.exists(anyLong())).thenReturn(true); + // Job template is available + when(mockJobTemplateService.isAvailable(any(), any(), any(), any())).thenReturn(true); // To successfully start a server there should be no running servers at first. // A subsequent call should eventually return a server in the ready state @@ -419,7 +476,7 @@ public void testStartServer_CreateUser_Success() throws Exception { when(mockJupyterHubClient.getServer(anyString(), anyString())).thenReturn(Optional.of(Server.builder().ready(true).build())); // Test - jupyterHubService.startServer(user, XnatProjectdata.SCHEMA_ELEMENT_NAME, projectId, projectId, projectId, eventTrackingId, profileId); + jupyterHubService.startServer(user, startProjectRequest); Thread.sleep(2500); // Async call, need to wait. Is there a better way to test this? // Verify create user attempt @@ -427,7 +484,8 @@ public void testStartServer_CreateUser_Success() throws Exception { // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), - eq(projectId), eq(projectId), eq(profileId), eq(eventTrackingId)); + eq(projectId), eq(projectId), eq(computeSpecConfigId), + eq(hardwareConfigId), eq(eventTrackingId)); // Verify JupyterHub start server request sent verify(mockJupyterHubClient, times(1)).startServer(eq(username), eq(""), any(UserOptions.class)); diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileServiceTest.java deleted file mode 100644 index 3b8db2e..0000000 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultProfileServiceTest.java +++ /dev/null @@ -1,285 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.services.impl; - -import org.junit.After; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.nrg.xapi.exceptions.DataFormatException; -import org.nrg.xnatx.plugins.jupyterhub.config.DefaultProfileServiceConfig; -import org.nrg.xnatx.plugins.jupyterhub.entities.ProfileEntity; -import org.nrg.xnatx.plugins.jupyterhub.models.Profile; -import org.nrg.xnatx.plugins.jupyterhub.models.docker.ContainerSpec; -import org.nrg.xnatx.plugins.jupyterhub.models.docker.Placement; -import org.nrg.xnatx.plugins.jupyterhub.models.docker.Resources; -import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; -import org.nrg.xnatx.plugins.jupyterhub.services.ProfileEntityService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = DefaultProfileServiceConfig.class) -public class DefaultProfileServiceTest { - - @Autowired private DefaultProfileService defaultProfileService; - @Autowired private ProfileEntityService mockProfileEntityService; - - @After - public void tearDown() { - Mockito.reset(mockProfileEntityService); - } - - @Test - public void exists() { - // create a profileEntity with an id - final Long profileId = 1L; - final ProfileEntity profileEntity = new ProfileEntity(); - profileEntity.setId(profileId); - - // mock the profileEntityService.retrieve() method to return a profileEntity - Mockito.when(mockProfileEntityService.retrieve(profileId)).thenReturn(profileEntity); - - // call the method under test - final boolean profileExists = defaultProfileService.exists(profileId); - - // verify profileEntityService.retrieve() is called - Mockito.verify(mockProfileEntityService, Mockito.times(1)).retrieve(profileId); - - // verify the method under test returns true - assert(profileExists); - } - - @Test - public void doesNotExist() { - // set the profile id - final Long profileId = 1L; - - // mock the profileEntityService.retrieve() method to return null - Mockito.when(mockProfileEntityService.retrieve(anyLong())).thenReturn(null); - - // call the method under test - final boolean profileExists = defaultProfileService.exists(profileId); - - // verify profileEntityService.retrieve() is called - Mockito.verify(mockProfileEntityService, Mockito.times(1)).retrieve(profileId); - - // verify the method under test returns false - assert(!profileExists); - } - - @Test - public void create() throws Exception { - // Setup profile - final Profile profile = createProfile(null, "profile1", "description1", "image1"); - - final ProfileEntity profileEntity = new ProfileEntity(); - profileEntity.setId(1L); - profileEntity.setProfile(profile); - - // mock the profileEntityService.create() method to return a profileEntity - Mockito.when(mockProfileEntityService.create(any(ProfileEntity.class))).thenReturn(profileEntity); - - // call the method under test - final Long profileId = defaultProfileService.create(profile); - - // verify profileEntityService.create() is called - Mockito.verify(mockProfileEntityService, Mockito.times(1)).create(profileEntity); - - // verify the method under test returns the correct profile id - assert(profileId.equals(profileEntity.getId())); - - // verify the profile id is set - assert(profile.getId().equals(profileEntity.getId())); - } - - @Test(expected = DataFormatException.class) - public void create_invalid() throws Exception { - // Setup profile - final Profile profile = createProfile(null, null, null, null); - - // call the method under test - defaultProfileService.create(profile); - } - - @Test - public void get() { - // Setup profile - final Profile profile = createProfile(1L, "profile1", "description1", "image1"); - - final ProfileEntity profileEntity = new ProfileEntity(); - profileEntity.setId(profile.getId()); - profileEntity.setProfile(profile); - - // mock the profileEntityService.retrieve() method to return a profileEntity - Mockito.when(mockProfileEntityService.retrieve(profile.getId())).thenReturn(profileEntity); - - // call the method under test - final Optional profileOptional = defaultProfileService.get(profile.getId()); - - // verify profileEntityService.retrieve() is called - Mockito.verify(mockProfileEntityService, Mockito.times(1)).retrieve(profile.getId()); - - // verify the method under test returns the correct profile - assert(profileOptional.isPresent()); - assert(profileOptional.get().getId().equals(profile.getId())); - assert(profileOptional.get().getName().equals(profile.getName())); - } - - @Test - public void get_doesNotExist() { - // set the profile id - final Long profileId = 1L; - - // mock the profileEntityService.retrieve() method to return null - Mockito.when(mockProfileEntityService.retrieve(anyLong())).thenReturn(null); - - // call the method under test - final Optional profileOptional = defaultProfileService.get(profileId); - - // verify profileEntityService.retrieve() is called - Mockito.verify(mockProfileEntityService, Mockito.times(1)).retrieve(profileId); - - // verify the method under test returns an empty optional - assert(!profileOptional.isPresent()); - } - - @Test - public void getAll() { - // Setup profiles - final Profile profile1 = createProfile(1L, "profile1", "description1", "image1"); - final Profile profile2 = createProfile(2L, "profile2", "description2", "image2"); - - final ProfileEntity profileEntity1 = new ProfileEntity(); - profileEntity1.setId(profile1.getId()); - profileEntity1.setProfile(profile1); - - final ProfileEntity profileEntity2 = new ProfileEntity(); - profileEntity2.setId(profile2.getId()); - profileEntity2.setProfile(profile2); - - final List profileEntities = Arrays.asList(profileEntity1, profileEntity2); - - // mock the profileEntityService.getAll() method to return a list of profileEntities - Mockito.when(mockProfileEntityService.getAll()).thenReturn(profileEntities); - - // call the method under test - final List profiles = defaultProfileService.getAll(); - - // verify profileEntityService.getAll() is called - Mockito.verify(mockProfileEntityService, Mockito.times(1)).getAll(); - - // verify the method under test returns the correct list of profiles - assert(profiles.size() == 2); - assert(profiles.get(0).getId().equals(profile1.getId())); - assert(profiles.get(0).getName().equals(profile1.getName())); - assert(profiles.get(1).getId().equals(profile2.getId())); - assert(profiles.get(1).getName().equals(profile2.getName())); - } - - @Test - public void update() throws DataFormatException { - // Setup profile - final Long profileId = 1L; - - final Profile oldProfile = createProfile(profileId, "oldName", "oldDescription", "oldImage"); - final Profile updatedProfile = createProfile(profileId, "updatedName", "updatedDescription", "updatedImage"); - - // Setup profileEntity - final ProfileEntity profileEntity = new ProfileEntity(); - profileEntity.setId(profileId); - profileEntity.setProfile(oldProfile); - - // mock the profileEntityService.retrieve() method to return a profileEntity - Mockito.when(mockProfileEntityService.retrieve(profileId)).thenReturn(profileEntity); - - // call the method under test - defaultProfileService.update(updatedProfile); - - // verify profileEntityService.retrieve() is called - Mockito.verify(mockProfileEntityService, Mockito.times(1)).retrieve(profileId); - - // verify profileEntityService.update() is called - Mockito.verify(mockProfileEntityService, Mockito.times(1)).update(profileEntity); - - // verify the profileEntity has the updated profile - assert(profileEntity.getProfile().getId().equals(updatedProfile.getId())); - assert(profileEntity.getProfile().getName().equals(updatedProfile.getName())); - } - - @Test(expected = DataFormatException.class) - public void update_invalid() throws Exception { - // Setup profile - final Long profileId = 1L; - - final Profile updatedProfile = createProfile(profileId, null, null, null); - - // call the method under test - defaultProfileService.update(updatedProfile); - } - - @Test(expected = DataFormatException.class) - public void update_doesNotExist() throws Exception { - // Setup profile - final Long profileId = 1L; - - final Profile updatedProfile = createProfile(profileId, "updatedName", "updatedDescription", "updatedImage"); - - // call the method under test - defaultProfileService.update(updatedProfile); - } - - @Test - public void delete() { - // set profile id - final Long profileId = 1L; - - // call the method under test - defaultProfileService.delete(profileId); - - // verify profileEntityService.delete() is called - Mockito.verify(mockProfileEntityService, Mockito.times(1)).delete(profileId); - } - - private Profile createProfile(Long id, String name, String description, String image) { - ContainerSpec containerSpec = ContainerSpec.builder() - .image(image) - .mounts(Collections.emptyList()) - .env(Collections.emptyMap()) - .labels(Collections.emptyMap()) - .build(); - - Placement placement = Placement.builder() - .constraints(Collections.emptyList()) - .build(); - - Resources resources = Resources.builder() - .cpuLimit(null) - .cpuReservation(null) - .memLimit(null) - .memReservation(null) - .genericResources(Collections.emptyMap()) - .build(); - - TaskTemplate taskTemplate = TaskTemplate.builder() - .containerSpec(containerSpec) - .placement(placement) - .resources(resources) - .build(); - - return Profile.builder() - .id(id) - .name(name) - .description(description) - .taskTemplate(taskTemplate) - .build(); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java index 1d53950..ac33e9d 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java @@ -1,19 +1,19 @@ package org.nrg.xnatx.plugins.jupyterhub.services.impl; -import org.junit.Before; -import org.junit.Ignore; +import org.apache.commons.lang3.StringUtils; import org.junit.Test; import org.junit.runner.RunWith; -import org.nrg.xft.security.UserI; +import org.nrg.jobtemplates.models.*; import org.nrg.xnatx.plugins.jupyterhub.config.DefaultUserOptionsServiceConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import java.util.ArrayList; -import java.util.List; +import java.util.*; -import static org.mockito.Mockito.mock; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = DefaultUserOptionsServiceConfig.class) @@ -21,28 +21,152 @@ public class DefaultUserOptionsServiceTest { @Autowired private DefaultUserOptionsService userOptionsService; - private UserI user = mock(UserI.class); + @Test + public void testToTaskTemplate() { + // Setup hardware + Hardware hardware1 = Hardware.builder() + .name("Small") + .cpuReservation(2.0) + .cpuLimit(4.0) + .memoryReservation("4G") + .memoryLimit("8G") + .build(); - private static final List projectIds = new ArrayList<>(); - static { - projectIds.add("ProjectA"); - projectIds.add("ProjectB"); - } + // Setup hardware constraints + Constraint hardwareConstraint1 = Constraint.builder() + .key("node.hardware") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Collections.singletonList("gpu"))) + .build(); - @Before - public void before() { - } + Constraint hardwareConstraint2 = Constraint.builder() + .key("node.labels") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Collections.singletonList("special"))) + .build(); - // TODO Write tests + hardware1.setConstraints(Arrays.asList(hardwareConstraint1, hardwareConstraint2)); - @Test @Ignore - public void testProjectPaths() { - // How to test XnatProjectdata static methods and getRootrchivePath and getCurrentArc? - } + // Setup hardware environment variables + EnvironmentVariable hardwareEnvironmentVariable1 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); + EnvironmentVariable hardwareEnvironmentVariable2 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); + + hardware1.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable1, hardwareEnvironmentVariable2)); + + // Setup hardware generic resources + GenericResource hardwareGenericResource1 = new GenericResource("nvidia.com/gpu", "2"); + GenericResource hardwareGenericResource2 = new GenericResource("fpga.com/fpga", "1"); + + hardware1.setGenericResources(Arrays.asList(hardwareGenericResource1, hardwareGenericResource2)); + + // Setup Mount + Mount mount1 = Mount.builder() + .localPath("/data/xnat/archive/Project1") + .containerPath("/data/xnat/archive/Project1") + .readOnly(true) + .build(); + + Mount mount2 = Mount.builder() + .localPath("/data/xnat/workspaces/users/andy") + .containerPath("/workspaces/users/andy") + .readOnly(false) + .build(); + + // Setup first compute spec + ComputeSpec computeSpec1 = ComputeSpec.builder() + .name("Jupyter Datascience Notebook") + .image("jupyter/datascience-notebook:hub-3.0.0") + .environmentVariables(Collections.singletonList(new EnvironmentVariable("COMPUTE_SPEC", "1"))) + .mounts(Arrays.asList(mount1, mount2)) + .build(); + + Constraint constraint1 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Arrays.asList("worker"))) + .build(); - @Test @Ignore public void testSubjectPaths() {} - @Test @Ignore public void testExperimentPaths() {} - @Test @Ignore public void testStoredSearchPaths() {} + Constraint constraint2 = Constraint.builder() + .key("node.labels.project") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Arrays.asList("ProjectA", "ProjectB"))) + .build(); + + Constraint constraint3 = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + // Now create the job template + JobTemplate template = JobTemplate.builder() + .computeSpec(computeSpec1) + .hardware(hardware1) + .constraints(Arrays.asList(constraint1, constraint2, constraint3)) + .build(); + + // Run the test + TaskTemplate result = userOptionsService.toTaskTemplate(template); + + // Verify the results + assertEquals(computeSpec1.getImage(), result.getContainerSpec().getImage()); + + // Verify the environment variables from the hardware and compute spec are merged + Map expectedEnv = new HashMap<>(); + expectedEnv.put("COMPUTE_SPEC", "1"); + expectedEnv.put("MATLAB_LICENSE_FILE", "12345@myserver"); + expectedEnv.put("NVIDIA_VISIBLE_DEVICES", "all"); + assertEquals(expectedEnv, result.getContainerSpec().getEnv()); + + // Verify the mounts from the hardware and compute spec are merged + assertEquals(2, result.getContainerSpec().getMounts().size()); + assertEquals("/data/xnat/archive/Project1", result.getContainerSpec().getMounts().get(0).getSource()); + assertEquals("/data/xnat/archive/Project1", result.getContainerSpec().getMounts().get(0).getTarget()); + assertEquals(true, result.getContainerSpec().getMounts().get(0).isReadOnly()); + assertEquals("/data/xnat/workspaces/users/andy", result.getContainerSpec().getMounts().get(1).getSource()); + assertEquals("/workspaces/users/andy", result.getContainerSpec().getMounts().get(1).getTarget()); + assertEquals(false, result.getContainerSpec().getMounts().get(1).isReadOnly()); + + // Check resources + assertEquals(hardware1.getCpuLimit(), result.getResources().getCpuLimit()); + assertEquals(hardware1.getCpuReservation(), result.getResources().getCpuReservation()); + assertEquals(hardware1.getMemoryLimit(), result.getResources().getMemLimit()); + assertEquals(hardware1.getMemoryReservation(), result.getResources().getMemReservation()); + assertEquals(2, result.getResources().getGenericResources().size()); + assertTrue(result.getResources().getGenericResources().entrySet().stream().anyMatch(e -> e.getKey().equals("nvidia.com/gpu") && e.getValue().equals("2"))); + assertTrue(result.getResources().getGenericResources().entrySet().stream().anyMatch(e -> e.getKey().equals("fpga.com/fpga") && e.getValue().equals("1"))); + + // Check constraints + assertEquals(7, result.getPlacement().getConstraints().size()); + assertTrue(result.getPlacement() + .getConstraints().stream() + .map(StringUtils::deleteWhitespace) + .anyMatch(c -> c.equals("node.role==worker"))); + assertTrue(result.getPlacement() + .getConstraints().stream() + .map(StringUtils::deleteWhitespace) + .anyMatch(c -> c.equals("node.labels.project==ProjectA"))); + assertTrue(result.getPlacement() + .getConstraints().stream() + .map(StringUtils::deleteWhitespace) + .anyMatch(c -> c.equals("node.labels.project==ProjectB"))); + assertTrue(result.getPlacement() + .getConstraints().stream() + .map(StringUtils::deleteWhitespace) + .anyMatch(c -> c.equals("node.instance.type!=spot"))); + assertTrue(result.getPlacement() + .getConstraints().stream() + .map(StringUtils::deleteWhitespace) + .anyMatch(c -> c.equals("node.instance.type!=demand"))); + assertTrue(result.getPlacement() + .getConstraints().stream() + .map(StringUtils::deleteWhitespace) + .anyMatch(c -> c.equals("node.hardware==gpu"))); + assertTrue(result.getPlacement() + .getConstraints().stream() + .map(StringUtils::deleteWhitespace) + .anyMatch(c -> c.equals("node.labels!=special"))); + } From fb0b639bf6c5675b3ac581252568f24f0995f068 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 15 May 2023 14:25:39 -0500 Subject: [PATCH 14/49] JHP-37: Turn off debug logging --- .../client/DefaultJupyterHubClient.java | 16 ++++++++-------- src/main/resources/jupyterhub-logback.xml | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/DefaultJupyterHubClient.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/DefaultJupyterHubClient.java index 2a3ae95..cf7a838 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/DefaultJupyterHubClient.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/DefaultJupyterHubClient.java @@ -50,7 +50,7 @@ public Hub getVersion() { log.trace("JupyterHub version received."); return response.getBody(); } catch (Exception e) { - log.error("", e); + log.debug("Unable to get JupyterHub version", e); throw new RuntimeException(e); } } @@ -101,7 +101,7 @@ public User createUser(final String username) { return response.getBody(); } catch (RestClientException e) { - log.error("Unable to create user on JupyterHub", e); + log.debug("Unable to create user on JupyterHub", e); throw new RuntimeException(e); } } @@ -127,7 +127,7 @@ public List getUsers() { return Arrays.asList(response.getBody()); } catch (RestClientException e) { - log.error("Unable to get users from JupyterHub", e); + log.debug("Unable to get users from JupyterHub", e); throw new RuntimeException(e); } } @@ -154,11 +154,11 @@ public Optional getUser(String username) { log.debug("User {} does not exist on JupyterHub", username); return Optional.empty(); } else { - log.error("Unable to get user " + username + " from JupyterHub", e); + log.debug("Unable to get user " + username + " from JupyterHub", e); throw new RuntimeException(e); } } catch (Exception e) { - log.error("Unable to get user " + username + " from JupyterHub", e); + log.debug("Unable to get user " + username + " from JupyterHub", e); throw new RuntimeException(e); } } @@ -230,7 +230,7 @@ public void startServer(String username, String servername, UserOptions userOpti throw new RuntimeException(msg); } } catch (RestClientException e) { - log.error("Failed to start Jupyter Server " + servername + " for user " + username, e); + log.debug("Failed to start Jupyter Server " + servername + " for user " + username, e); throw new RuntimeException(e); } } @@ -261,7 +261,7 @@ public void stopServer(String username, String servername) { if (e.getStatusCode() == HttpStatus.NOT_FOUND) { log.debug("User {} / Server {} not found.", username, servername); } else { - log.error("Failed to stop Jupyter server", e); + log.debug("Failed to stop Jupyter server", e); throw e; } } @@ -295,7 +295,7 @@ public Token createToken(String username, Token token) { log.debug("Token created for user {}", username); return response.getBody(); } catch (Exception e) { - log.error("Unable to create token for user " + username, e); + log.debug("Unable to create token for user " + username, e); throw new RuntimeException(e); } } diff --git a/src/main/resources/jupyterhub-logback.xml b/src/main/resources/jupyterhub-logback.xml index 5177e33..ec35cc2 100644 --- a/src/main/resources/jupyterhub-logback.xml +++ b/src/main/resources/jupyterhub-logback.xml @@ -20,11 +20,10 @@ ${xnat.home}/logs/job-templates.log.%d{yyyy-MM-dd} - - + - + From c16d55c85d082c5c4627d0bbd67bedf4bb288796 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 15 May 2023 15:53:04 -0500 Subject: [PATCH 15/49] JHP-35: Two separate path translations for archive and workspace mounts --- .../MigratePathTranslationPreference.java | 100 ++++++++++ .../preferences/JupyterHubPreferences.java | 46 ++++- .../impl/DefaultUserOptionsService.java | 37 +++- .../spawner/jupyterhub/site-settings.yaml | 42 +++-- ...tePathTranslationPreferenceTestConfig.java | 29 +++ .../MigratePathTranslationPreferenceTest.java | 177 ++++++++++++++++++ .../JupyterHubPreferencesTest.java | 65 +++++++ .../impl/DefaultUserOptionsServiceTest.java | 46 ++++- 8 files changed, 511 insertions(+), 31 deletions(-) create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreference.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MigratePathTranslationPreferenceTestConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreferenceTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferencesTest.java diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreference.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreference.java new file mode 100644 index 0000000..736664f --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreference.java @@ -0,0 +1,100 @@ +package org.nrg.xnatx.plugins.jupyterhub.initialize; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.nrg.prefs.services.NrgPreferenceService; +import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; +import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Properties; + +/** + * Migrate the pathTranslationXnatPrefix preferences to the new pathTranslationArchivePrefix and pathTranslationWorkspacePrefix preferences. + * Migrate the pathTranslationDockerPrefix preferences to the new pathTranslationArchiveDockerPrefix and pathTranslationWorkspaceDockerPrefix preferences. + */ +@Component +@Slf4j +public class MigratePathTranslationPreference extends AbstractInitializingTask { + + private final NrgPreferenceService nrgPreferenceService; + private final JupyterHubPreferences jupyterHubPreferences; + private final XFTManagerHelper xftManagerHelper; + private final XnatAppInfo appInfo; + + @Autowired + public MigratePathTranslationPreference(final XFTManagerHelper xftManagerHelper, + final XnatAppInfo appInfo, + final NrgPreferenceService nrgPreferenceService, + final JupyterHubPreferences jupyterHubPreferences) { + this.xftManagerHelper = xftManagerHelper; + this.appInfo = appInfo; + this.nrgPreferenceService = nrgPreferenceService; + this.jupyterHubPreferences = jupyterHubPreferences; + } + + @Override + public String getTaskName() { + return "MigratePathTranslationPreference"; + } + + @Override + protected void callImpl() throws InitializingTaskException { + log.debug("Initializing JupyterHub preferences."); + + // Not sure if I need to check both of these or just one. + if (!xftManagerHelper.isInitialized()) { + log.debug("XFT not initialized, deferring execution."); + throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); + } + + if (!appInfo.isInitialized()) { + log.debug("XNAT not initialized, deferring execution."); + throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); + } + + // Migrate the old pathTranslation preferences to the new ones. + // Delete the old preferences after migrating them. + try { + Properties properties = nrgPreferenceService.getToolProperties(JupyterHubPreferences.TOOL_ID); + if (!properties.isEmpty()) { + for(String key: properties.stringPropertyNames()) { + if (key.startsWith("pathTranslationXnatPrefix")) { + if (StringUtils.isBlank(properties.getProperty(key))) { + // No need to migrate if the value is blank, but we should delete the old preference. + nrgPreferenceService.deletePreference(JupyterHubPreferences.TOOL_ID, key); + continue; + } + + jupyterHubPreferences.setPathTranslationArchivePrefix(properties.getProperty(key)); + jupyterHubPreferences.setPathTranslationWorkspacePrefix(properties.getProperty(key)); + log.debug("Migrated pathTranslationXnatPrefix preference."); + + nrgPreferenceService.deletePreference(JupyterHubPreferences.TOOL_ID, key); + log.debug("Deleted pathTranslationXnatPrefix preference."); + } else if (key.startsWith("pathTranslationDockerPrefix")) { + if (StringUtils.isBlank(properties.getProperty(key))) { + // No need to migrate if the value is blank, but we do need to delete the preference. + nrgPreferenceService.deletePreference(JupyterHubPreferences.TOOL_ID, key); + continue; + } + + jupyterHubPreferences.setPathTranslationArchiveDockerPrefix(properties.getProperty(key)); + jupyterHubPreferences.setPathTranslationWorkspaceDockerPrefix(properties.getProperty(key)); + log.debug("Migrated pathTranslationDockerPrefix preference."); + + nrgPreferenceService.deletePreference(JupyterHubPreferences.TOOL_ID, key); + log.debug("Deleted pathTranslationDockerPrefix preference."); + } + } + } + } catch (Exception e) { + throw new InitializingTaskException(InitializingTaskException.Level.Error, "Error migrating pathTranslation preferences.", e); + } + + } +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java index b7e7c05..0f4f417 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java @@ -135,28 +135,28 @@ public void getStopPollingInterval(final Integer stopPollingInterval) { } @NrgPreference(defaultValue = "") - public String getPathTranslationXnatPrefix() { - return getValue("pathTranslationXnatPrefix"); + public String getPathTranslationArchivePrefix() { + return getValue("pathTranslationArchivePrefix"); } - public void setPathTranslationXnatPrefix(final String pathTranslationXnatPrefix) { + public void setPathTranslationArchivePrefix(final String pathTranslationArchivePrefix) { try { - set(pathTranslationXnatPrefix, "pathTranslationXnatPrefix"); + set(pathTranslationArchivePrefix, "pathTranslationArchivePrefix"); } catch (InvalidPreferenceName e) { - log.error("Invalid preference name 'pathTranslationXnatPrefix': something is very wrong here.", e); + log.error("Invalid preference name 'pathTranslationArchivePrefix': something is very wrong here.", e); } } @NrgPreference(defaultValue = "") - public String getPathTranslationDockerPrefix() { - return getValue("pathTranslationDockerPrefix"); + public String getPathTranslationArchiveDockerPrefix() { + return getValue("pathTranslationArchiveDockerPrefix"); } - public void setPathTranslationDockerPrefix(final String pathTranslationDockerPrefix) { + public void setPathTranslationArchiveDockerPrefix(final String pathTranslationArchiveDockerPrefix) { try { - set(pathTranslationDockerPrefix, "pathTranslationDockerPrefix"); + set(pathTranslationArchiveDockerPrefix, "pathTranslationArchiveDockerPrefix"); } catch (InvalidPreferenceName e) { - log.error("Invalid preference name 'pathTranslationDockerPrefix': something is very wrong here.", e); + log.error("Invalid preference name 'pathTranslationArchiveDockerPrefix': something is very wrong here.", e); } } @@ -165,6 +165,32 @@ public String getWorkspacePath() { return getValue("workspacePath"); } + @NrgPreference(defaultValue = "") + public String getPathTranslationWorkspacePrefix() { + return getValue("pathTranslationWorkspacePrefix"); + } + + public void setPathTranslationWorkspacePrefix(final String pathTranslationWorkspacePrefix) { + try { + set(pathTranslationWorkspacePrefix, "pathTranslationWorkspacePrefix"); + } catch (InvalidPreferenceName e) { + log.error("Invalid preference name 'pathTranslationWorkspacePrefix': something is very wrong here.", e); + } + } + + @NrgPreference(defaultValue = "") + public String getPathTranslationWorkspaceDockerPrefix() { + return getValue("pathTranslationWorkspaceDockerPrefix"); + } + + public void setPathTranslationWorkspaceDockerPrefix(final String pathTranslationWorkspaceDockerPrefix) { + try { + set(pathTranslationWorkspaceDockerPrefix, "pathTranslationWorkspaceDockerPrefix"); + } catch (InvalidPreferenceName e) { + log.error("Invalid preference name 'pathTranslationWorkspaceDockerPrefix': something is very wrong here.", e); + } + } + public void setWorkspacePath(final String workspacePath) { try { set(workspacePath, "workspacePath"); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java index 12704f9..8bab1c8 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java @@ -321,7 +321,7 @@ public void storeUserOptions(UserI user, String servername, String xsiType, Stri // Add user workspace mount final Mount userNotebookDirectoryMount = Mount.builder() - .source(translatePath(workspacePath.toString())) + .source(translateWorkspacePath(workspacePath.toString())) .target(Paths.get("/workspace", user.getUsername()).toString()) .type("bind") .readOnly(false) @@ -330,7 +330,7 @@ public void storeUserOptions(UserI user, String servername, String xsiType, Stri // Add xnat data mounts final List xnatDataMounts = paths.entrySet().stream() .map((entry) -> Mount.builder() - .source(translatePath(entry.getValue())) + .source(translateArchivePath(entry.getValue())) .target(Paths.get(entry.getKey()).toString()) .type("bind") .readOnly(true) @@ -364,12 +364,35 @@ public void storeUserOptions(UserI user, String servername, String xsiType, Stri userOptionsEntityService.createOrUpdate(userOptionsEntity); } - private String translatePath(String path) { - String pathTranslationXnatPrefix = jupyterHubPreferences.getPathTranslationXnatPrefix(); - String pathTranslationDockerPrefix = jupyterHubPreferences.getPathTranslationDockerPrefix(); + /** + * Translate paths within the archive to paths within the docker container + * @param path the path to translate, must be the archive path or a subdirectory of the archive path + * @return the translated path + */ + protected String translateArchivePath(String path) { + String pathTranslationArchivePrefix = jupyterHubPreferences.getPathTranslationArchivePrefix(); + String pathTranslationArchiveDockerPrefix = jupyterHubPreferences.getPathTranslationArchiveDockerPrefix(); + + if (StringUtils.isNotBlank(pathTranslationArchivePrefix) && + StringUtils.isNotBlank(pathTranslationArchiveDockerPrefix)) { + return path.replace(pathTranslationArchivePrefix, pathTranslationArchiveDockerPrefix); + } else { + return path; + } + } - if (!StringUtils.isEmpty(pathTranslationXnatPrefix) && !StringUtils.isEmpty(pathTranslationDockerPrefix)) { - return path.replace(pathTranslationXnatPrefix, pathTranslationDockerPrefix); + /** + * Translate paths within the workspace to paths within the docker container + * @param path the path to translate, must be the workspace path or a subdirectory of the workspace path + * @return the translated path + */ + protected String translateWorkspacePath(String path) { + String pathTranslationWorkspacePrefix = jupyterHubPreferences.getPathTranslationWorkspacePrefix(); + String pathTranslationWorkspaceDockerPrefix = jupyterHubPreferences.getPathTranslationWorkspaceDockerPrefix(); + + if (StringUtils.isNotBlank(pathTranslationWorkspacePrefix) && + StringUtils.isNotBlank(pathTranslationWorkspaceDockerPrefix)) { + return path.replace(pathTranslationWorkspacePrefix, pathTranslationWorkspaceDockerPrefix); } else { return path; } diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index 4848444..4e6e170 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -61,21 +61,37 @@ stopPollingInterval: description: > The rate (in seconds) at which to poll JupyterHub to check if the single-user server has stopped. -pathTranslationXnatPrefix: +pathTranslationArchivePrefix: kind: panel.input.text - id: pathTranslationXnatPrefix - name: pathTranslationXnatPrefix - label: Path Translation XNAT Prefix + id: pathTranslationArchivePrefix + name: pathTranslationArchivePrefix + label: Path Translation XNAT Archive Prefix description: > - (Optional) Enter the XNAT_HOME server path, i.e. "/data/xnat" + (Optional) Enter the XNAT archive path as seen by XNAT, i.e. "/data/xnat/archive" -pathTranslationDockerPrefix: +pathTranslationArchiveDockerPrefix: + kind: panel.input.text + id: pathTranslationArchiveDockerPrefix + name: pathTranslationArchiveDockerPrefix + label: Path Translation Docker Archive Prefix + description: > + (Optional) Enter the Docker Server path to the XNAT archive mount, i.e. "/docker/my-data/XNAT/archive" + +pathTranslationWorkspacePrefix: kind: panel.input.text - id: pathTranslationDockerPrefix - name: pathTranslationDockerPrefix - label: Path Translation Docker Prefix + id: pathTranslationWorkspacePrefix + name: pathTranslationWorkspacePrefix + label: Path Translation User Workspace Prefix description: > - (Optional) Enter the Docker Server path to the XNAT_HOME mount, i.e. "/docker/my-data/XNAT" + (Optional) Enter the user workspace path as seen by XNAT, i.e. "/data/xnat/workspaces" + +pathTranslationWorkspaceDockerPrefix: + kind: panel.input.text + id: pathTranslationWorkspaceDockerPrefix + name: pathTranslationWorkspaceDockerPrefix + label: Path Translation Docker User Workspace Prefix + description: > + (Optional) Enter the Docker Server path to the user workspace mount, i.e. "/docker/my-data/XNAT/workspaces" inactivityTimeout: kind: panel.input.number @@ -229,9 +245,11 @@ jupyterhubPreferences: ${stopTimeout} ${inactivityTimeout} ${maxServerLifetime} - ${pathTranslationXnatPrefix} - ${pathTranslationDockerPrefix} ${workspacePath} + ${pathTranslationArchivePrefix} + ${pathTranslationArchiveDockerPrefix} + ${pathTranslationWorkspacePrefix} + ${pathTranslationWorkspaceDockerPrefix} jupyterHubUserActivity: kind: panel diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MigratePathTranslationPreferenceTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MigratePathTranslationPreferenceTestConfig.java new file mode 100644 index 0000000..4b57f7c --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MigratePathTranslationPreferenceTestConfig.java @@ -0,0 +1,29 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.prefs.services.NrgPreferenceService; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.initialize.MigratePathTranslationPreference; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({MockConfig.class}) +public class MigratePathTranslationPreferenceTestConfig { + + @Bean + public MigratePathTranslationPreference defaultMigratePathTranslationPreferenceTest(final XFTManagerHelper mockXFTManagerHelper, + final XnatAppInfo mockXnatAppInfo, + final NrgPreferenceService nrgPreferenceService, + final JupyterHubPreferences jupyterHubPreferences) { + return new MigratePathTranslationPreference( + mockXFTManagerHelper, + mockXnatAppInfo, + nrgPreferenceService, + jupyterHubPreferences + ); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreferenceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreferenceTest.java new file mode 100644 index 0000000..6a1ef66 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreferenceTest.java @@ -0,0 +1,177 @@ +package org.nrg.xnatx.plugins.jupyterhub.initialize; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.prefs.services.NrgPreferenceService; +import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.config.MigratePathTranslationPreferenceTestConfig; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Properties; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = MigratePathTranslationPreferenceTestConfig.class) +public class MigratePathTranslationPreferenceTest { + + @Autowired private XFTManagerHelper mockXFTManagerHelper; + @Autowired private XnatAppInfo mockXnatAppInfo; + @Autowired private NrgPreferenceService mockNrgPreferenceService; + @Autowired private JupyterHubPreferences mockJupyterHubPreferences; + @Autowired private MigratePathTranslationPreference migratePathTranslationPreference; + + @Before + public void before() { + assertNotNull(mockXFTManagerHelper); + assertNotNull(mockXnatAppInfo); + assertNotNull(mockNrgPreferenceService); + assertNotNull(mockJupyterHubPreferences); + assertNotNull(migratePathTranslationPreference); + } + + @After + public void after() { + resetMocks(); + } + + private void resetMocks() { + Mockito.reset( + mockXFTManagerHelper, + mockXnatAppInfo, + mockNrgPreferenceService, + mockJupyterHubPreferences + ); + } + + @Test + public void testGetTaskName() { + // Test that the task name is not null + final String taskName = migratePathTranslationPreference.getTaskName(); + assertNotNull(taskName); + } + + @Test + public void testCallImpl_XnatInitialized_NoExceptions() throws Exception { + // XFT initialized and app initialized, we should not throw an exception + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + when(mockNrgPreferenceService.getToolProperties(any())).thenReturn(new Properties()); + + // Should not throw InitializingTaskException + migratePathTranslationPreference.callImpl(); + } + + @Test(expected = InitializingTaskException.class) + public void testCallImpl_XnatNotInitialized_Exception1() throws Exception { + // XFT not initialized, we should throw an exception + when(mockXFTManagerHelper.isInitialized()).thenReturn(false); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + when(mockNrgPreferenceService.getToolProperties(any())).thenReturn(new Properties()); + + // Should throw InitializingTaskException + migratePathTranslationPreference.callImpl(); + } + + @Test(expected = InitializingTaskException.class) + public void testCallImpl_XnatNotInitialized_Exception2() throws Exception { + // XFT initialized, app not initialized, we should throw an exception + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(false); + when(mockNrgPreferenceService.getToolProperties(any())).thenReturn(new Properties()); + + // Should throw InitializingTaskException + migratePathTranslationPreference.callImpl(); + } + + @Test + public void testCallImpl_MigrateOldPreferences() throws Exception { + // XFT initialized and app initialized, we should not throw an exception + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + + Properties properties = new Properties(); + properties.setProperty("pathTranslationXnatPrefix", "/data/pixi/archive"); + properties.setProperty("pathTranslationDockerPrefix", "/data/xnat/archive"); + + when(mockNrgPreferenceService.getToolProperties(any())).thenReturn(properties); + + // Should not throw InitializingTaskException + migratePathTranslationPreference.callImpl(); + + // Verify migration + verify(mockJupyterHubPreferences).setPathTranslationArchivePrefix("/data/pixi/archive"); + verify(mockJupyterHubPreferences).setPathTranslationWorkspacePrefix("/data/pixi/archive"); + verify(mockJupyterHubPreferences).setPathTranslationArchiveDockerPrefix("/data/xnat/archive"); + verify(mockJupyterHubPreferences).setPathTranslationWorkspaceDockerPrefix("/data/xnat/archive"); + + // Verify deletion of old preferences + verify(mockNrgPreferenceService).deletePreference(JupyterHubPreferences.TOOL_ID, "pathTranslationXnatPrefix"); + verify(mockNrgPreferenceService).deletePreference(JupyterHubPreferences.TOOL_ID, "pathTranslationDockerPrefix"); + } + + @Test + public void testCallImpl_DontMigrateEmptyPreferences() throws Exception { + // XFT initialized and app initialized, we should not throw an exception + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + + // Test with blank values + Properties properties = new Properties(); + properties.setProperty("pathTranslationXnatPrefix", ""); + properties.setProperty("pathTranslationDockerPrefix", ""); + + when(mockNrgPreferenceService.getToolProperties(any())).thenReturn(properties); + + // Should not throw InitializingTaskException + migratePathTranslationPreference.callImpl(); + + // Verify migration did not occur + verify(mockJupyterHubPreferences, never()).setPathTranslationArchivePrefix(""); + verify(mockJupyterHubPreferences, never()).setPathTranslationWorkspacePrefix(""); + verify(mockJupyterHubPreferences, never()).setPathTranslationArchiveDockerPrefix(""); + verify(mockJupyterHubPreferences, never()).setPathTranslationWorkspaceDockerPrefix(""); + + // Verify deletion of old preferences did occur + verify(mockNrgPreferenceService).deletePreference(JupyterHubPreferences.TOOL_ID, "pathTranslationXnatPrefix"); + verify(mockNrgPreferenceService).deletePreference(JupyterHubPreferences.TOOL_ID, "pathTranslationDockerPrefix"); + } + + @Test + public void testCallImpl_DontMigrateEmptyPreferences2() throws Exception { + // XFT initialized and app initialized, we should not throw an exception + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + + // Test with blank values + Properties properties = new Properties(); + properties.setProperty("pathTranslationWorkspacePrefix", "/data/pixi/workspaces"); + properties.setProperty("pathTranslationWorkspaceDockerPrefix", "/data/xnat/workspaces"); + properties.setProperty("resourceSpecCpuLimit", "1"); + + when(mockNrgPreferenceService.getToolProperties(any())).thenReturn(properties); + + // Should not throw InitializingTaskException + migratePathTranslationPreference.callImpl(); + + // Verify migration never called + verify(mockJupyterHubPreferences, never()).setPathTranslationArchivePrefix(""); + verify(mockJupyterHubPreferences, never()).setPathTranslationWorkspacePrefix(""); + verify(mockJupyterHubPreferences, never()).setPathTranslationArchiveDockerPrefix(""); + verify(mockJupyterHubPreferences, never()).setPathTranslationWorkspaceDockerPrefix(""); + + // Verify deletion of old preferences never called + verify(mockNrgPreferenceService, never()).deletePreference(JupyterHubPreferences.TOOL_ID, "pathTranslationXnatPrefix"); + verify(mockNrgPreferenceService, never()).deletePreference(JupyterHubPreferences.TOOL_ID, "pathTranslationDockerPrefix"); + } +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferencesTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferencesTest.java new file mode 100644 index 0000000..3c322d7 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferencesTest.java @@ -0,0 +1,65 @@ +package org.nrg.xnatx.plugins.jupyterhub.preferences; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.framework.configuration.ConfigPaths; +import org.nrg.framework.utilities.OrderedProperties; +import org.nrg.prefs.services.NrgPreferenceService; +import org.nrg.xnatx.plugins.jupyterhub.config.MockConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = MockConfig.class) +public class JupyterHubPreferencesTest { + + @Autowired private NrgPreferenceService mockNrgPreferenceService; + @Autowired private ConfigPaths mockConfigPaths; + @Autowired private OrderedProperties mockOrderedProperties; + + private JupyterHubPreferences jupyterHubPreferences; + + @Before + public void before() { + assertNotNull(mockNrgPreferenceService); + assertNotNull(mockConfigPaths); + assertNotNull(mockOrderedProperties); + jupyterHubPreferences = new JupyterHubPreferences(mockNrgPreferenceService, mockConfigPaths, mockOrderedProperties); + } + + @After + public void after() { + Mockito.reset( + mockNrgPreferenceService, + mockConfigPaths, + mockOrderedProperties + ); + } + + @Test + public void testGetPathTranslationArchivePrefix() { + jupyterHubPreferences.getPathTranslationArchivePrefix(); + } + + @Test + public void testGetPathTranslationArchiveDockerPrefix() { + jupyterHubPreferences.getPathTranslationArchiveDockerPrefix(); + } + + @Test + public void testGetPathTranslationWorkspacePrefix() { + jupyterHubPreferences.getPathTranslationWorkspacePrefix(); + } + + @Test + public void testGetPathTranslationWorkspaceDockerPrefix() { + jupyterHubPreferences.getPathTranslationWorkspaceDockerPrefix(); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java index ac33e9d..998afab 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java @@ -1,25 +1,67 @@ package org.nrg.xnatx.plugins.jupyterhub.services.impl; import org.apache.commons.lang3.StringUtils; +import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.nrg.jobtemplates.models.*; import org.nrg.xnatx.plugins.jupyterhub.config.DefaultUserOptionsServiceConfig; import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.*; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = DefaultUserOptionsServiceConfig.class) public class DefaultUserOptionsServiceTest { @Autowired private DefaultUserOptionsService userOptionsService; + @Autowired private JupyterHubPreferences mockJupyterHubPreferences; + + @After + public void after() { + Mockito.reset( + mockJupyterHubPreferences + ); + } + + @Test + public void testPathTranslationArchive() { + // Setup mocks + when(mockJupyterHubPreferences.getPathTranslationArchivePrefix()).thenReturn("/data/xnat/archive"); + when(mockJupyterHubPreferences.getPathTranslationArchiveDockerPrefix()).thenReturn("/docker/data/xnat/archive"); + when(mockJupyterHubPreferences.getPathTranslationWorkspacePrefix()).thenReturn("/data/xnat/workspaces"); + when(mockJupyterHubPreferences.getPathTranslationWorkspaceDockerPrefix()).thenReturn("/docker/data/xnat/workspaces"); + + // Test archive path translation + String translated = userOptionsService.translateArchivePath("/data/xnat/archive/Project1"); + + // Verify archive path translation + assertThat(translated, is(("/docker/data/xnat/archive/Project1"))); + } + + @Test + public void testPathTranslationWorkspace() { + // Setup mocks + when(mockJupyterHubPreferences.getPathTranslationArchivePrefix()).thenReturn("/data/xnat/archive"); + when(mockJupyterHubPreferences.getPathTranslationArchiveDockerPrefix()).thenReturn("/docker/data/xnat/archive"); + when(mockJupyterHubPreferences.getPathTranslationWorkspacePrefix()).thenReturn("/data/xnat/workspaces"); + when(mockJupyterHubPreferences.getPathTranslationWorkspaceDockerPrefix()).thenReturn("/docker/data/xnat/workspaces"); + + // Test workspace path translation + String translated = userOptionsService.translateWorkspacePath("/data/xnat/workspaces/users/andy"); + + // Verify workspace path translation + assertThat(translated, is(("/docker/data/xnat/workspaces/users/andy"))); + } @Test public void testToTaskTemplate() { From 8d98f275bbbab5f91ca82639a44f18219f8ae1d6 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Wed, 17 May 2023 15:27:20 -0500 Subject: [PATCH 16/49] Change the default notebook. Change column label in the JupyterHub plugin preferences --- .../JupyterHubEnvironmentsAndHardwareInitializer.java | 4 ++-- .../scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java index ca9082d..231db15 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java @@ -114,8 +114,8 @@ private ComputeSpecConfig buildDefaultComputeSpecConfig() { .build(); // Set the ComputeSpec values - computeSpec.setName("Jupyter Datascience Notebook"); - computeSpec.setImage("jupyter/datascience-notebook:hub-3.0.0"); + computeSpec.setName("XNAT Datascience Notebook"); + computeSpec.setImage("xnat/datascience-notebook:latest"); computeSpec.setEnvironmentVariables(new ArrayList<>()); computeSpec.setMounts(new ArrayList<>()); diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js index d5c89cb..0336471 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js @@ -88,7 +88,7 @@ XNAT.plugin.jupyterhub.hub = getObject(XNAT.plugin.jupyterhub.hub || {}); hubTable.tr() .th({addClass: 'left', html: 'API Path'}) .th('Status') - .th('Version') + .th('Hub Version') .th('Actions') function editButton() { From dc4f171c2a87436f14581248545bcbd5e4bf331e Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Fri, 19 May 2023 13:50:26 -0500 Subject: [PATCH 17/49] JHP-46: Add a check mark to the Ready column of the User Activity table instead of true/false --- .../scripts/xnat/plugin/jupyterhub/jupyterhub-users.js | 2 +- .../META-INF/xnat/spawner/jupyterhub/site-settings.yaml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js index de43f56..79a84c7 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js @@ -201,7 +201,7 @@ XNAT.plugin.jupyterhub.users.tokens = getObject(XNAT.plugin.jupyterhub.users.tok usersTable.tr() .td([spawn('div.left', [name])]) .td([spawn('div.center', [hasServer ? serverDialog(user['name'], servers['']) : ''])]) - .td([spawn('div.center', [ready])]) + .td([spawn('div.center', [ready ? spawn('i.fa.fa-check') : ''])]) .td([spawn('div.center', [started.toLocaleString()])]) .td([spawn('div.center', [lastActivity.toLocaleString()])]) .td([spawn('div.center', [hasServer ? stopServerButton(name, '') : ''])]); diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index 4e6e170..38c03ff 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -259,7 +259,8 @@ jupyterHubUserActivity: jupyterHubActivityDescription: tag: div.message contents: - "This panel lists any active Jupyter users and the status of their Jupyter notebook servers." + "This panel lists any active JupyterHub users and the status of their Jupyter notebook servers. You can + stop a user's Jupyter notebook server and view the settings for the user's Jupyter notebook server." jupyterHubActivityTable: tag: "div#jupyterhub-user-activity-table" imagePreferencesScript: From 08dcd7e07a9c1e9f5fc8698403275960d2b18d09 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Fri, 19 May 2023 15:57:01 -0500 Subject: [PATCH 18/49] JHP-46: Add a gear in the Ready column for the startup/stop state. Update User Activity panel description. --- .../scripts/xnat/plugin/jupyterhub/jupyterhub-users.js | 2 +- .../META-INF/xnat/spawner/jupyterhub/site-settings.yaml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js index 79a84c7..00cf194 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-users.js @@ -201,7 +201,7 @@ XNAT.plugin.jupyterhub.users.tokens = getObject(XNAT.plugin.jupyterhub.users.tok usersTable.tr() .td([spawn('div.left', [name])]) .td([spawn('div.center', [hasServer ? serverDialog(user['name'], servers['']) : ''])]) - .td([spawn('div.center', [ready ? spawn('i.fa.fa-check') : ''])]) + .td([spawn('div.center', [ready ? spawn('i.fa.fa-check') : spawn('i.fa.fa-gear')])]) .td([spawn('div.center', [started.toLocaleString()])]) .td([spawn('div.center', [lastActivity.toLocaleString()])]) .td([spawn('div.center', [hasServer ? stopServerButton(name, '') : ''])]); diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index 38c03ff..1879111 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -259,8 +259,10 @@ jupyterHubUserActivity: jupyterHubActivityDescription: tag: div.message contents: - "This panel lists any active JupyterHub users and the status of their Jupyter notebook servers. You can - stop a user's Jupyter notebook server and view the settings for the user's Jupyter notebook server." + "The User Activity panel displays all JupyterHub users and their active Jupyter notebook servers. You can stop + a user's server and also view a server's configuration details. The Started column indicates the server's launch + time, while the Last Activity column approximates the user's most recent interaction with the server. The Ready + column indicates whether the server is up and running or is in the process of starting or stopping." jupyterHubActivityTable: tag: "div#jupyterhub-user-activity-table" imagePreferencesScript: From 8ad8a97b5ed1467f249080ecb230dd3c876717e4 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 22 May 2023 15:58:41 -0500 Subject: [PATCH 19/49] JHP-35: Update migration paths --- .../initialize/MigratePathTranslationPreference.java | 8 ++++---- .../initialize/MigratePathTranslationPreferenceTest.java | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreference.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreference.java index 736664f..f9d8253 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreference.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreference.java @@ -70,8 +70,8 @@ protected void callImpl() throws InitializingTaskException { continue; } - jupyterHubPreferences.setPathTranslationArchivePrefix(properties.getProperty(key)); - jupyterHubPreferences.setPathTranslationWorkspacePrefix(properties.getProperty(key)); + jupyterHubPreferences.setPathTranslationArchivePrefix(properties.getProperty(key) + "/archive"); + jupyterHubPreferences.setPathTranslationWorkspacePrefix(properties.getProperty(key) + "/workspaces"); log.debug("Migrated pathTranslationXnatPrefix preference."); nrgPreferenceService.deletePreference(JupyterHubPreferences.TOOL_ID, key); @@ -83,8 +83,8 @@ protected void callImpl() throws InitializingTaskException { continue; } - jupyterHubPreferences.setPathTranslationArchiveDockerPrefix(properties.getProperty(key)); - jupyterHubPreferences.setPathTranslationWorkspaceDockerPrefix(properties.getProperty(key)); + jupyterHubPreferences.setPathTranslationArchiveDockerPrefix(properties.getProperty(key) + "/archive"); + jupyterHubPreferences.setPathTranslationWorkspaceDockerPrefix(properties.getProperty(key) + "/workspaces"); log.debug("Migrated pathTranslationDockerPrefix preference."); nrgPreferenceService.deletePreference(JupyterHubPreferences.TOOL_ID, key); diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreferenceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreferenceTest.java index 6a1ef66..9f3e33c 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreferenceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/MigratePathTranslationPreferenceTest.java @@ -101,8 +101,8 @@ public void testCallImpl_MigrateOldPreferences() throws Exception { when(mockXnatAppInfo.isInitialized()).thenReturn(true); Properties properties = new Properties(); - properties.setProperty("pathTranslationXnatPrefix", "/data/pixi/archive"); - properties.setProperty("pathTranslationDockerPrefix", "/data/xnat/archive"); + properties.setProperty("pathTranslationXnatPrefix", "/data/pixi"); + properties.setProperty("pathTranslationDockerPrefix", "/data/xnat"); when(mockNrgPreferenceService.getToolProperties(any())).thenReturn(properties); @@ -111,9 +111,9 @@ public void testCallImpl_MigrateOldPreferences() throws Exception { // Verify migration verify(mockJupyterHubPreferences).setPathTranslationArchivePrefix("/data/pixi/archive"); - verify(mockJupyterHubPreferences).setPathTranslationWorkspacePrefix("/data/pixi/archive"); + verify(mockJupyterHubPreferences).setPathTranslationWorkspacePrefix("/data/pixi/workspaces"); verify(mockJupyterHubPreferences).setPathTranslationArchiveDockerPrefix("/data/xnat/archive"); - verify(mockJupyterHubPreferences).setPathTranslationWorkspaceDockerPrefix("/data/xnat/archive"); + verify(mockJupyterHubPreferences).setPathTranslationWorkspaceDockerPrefix("/data/xnat/workspaces"); // Verify deletion of old preferences verify(mockNrgPreferenceService).deletePreference(JupyterHubPreferences.TOOL_ID, "pathTranslationXnatPrefix"); From f18fa680117625af46c97431448d48f3465b78b2 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 22 May 2023 16:20:51 -0500 Subject: [PATCH 20/49] JHP-48: Update User Authorization verbage --- .../META-INF/xnat/spawner/jupyterhub/site-settings.yaml | 8 ++++---- .../config/roles/ jupyter-role-definition.properties | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index 1879111..46840c2 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -138,12 +138,12 @@ userAuthorization: style: marginBottom: 24px html: - Users with access to Jupyter will be able to run arbitrary code from within their Jupyter notebook container. - Individual users may be authorized to start Jupyter containers or all users (excluding guest users) can be - authorized to start Jupyter containers. + Users with access to Jupyter will be able to run arbitrary code from within their Jupyter servers. + Individual users may be authorized to start Jupyter servers or all users (excluding guest users) can be + authorized to start Jupyter servers. allowAllToggle: kind: panel.input.switchbox - label: "All Users Can Start Jupyter" + label: "Enable Jupyter for all users" name: allowAll id: allowAll onText: false diff --git a/src/main/resources/config/roles/ jupyter-role-definition.properties b/src/main/resources/config/roles/ jupyter-role-definition.properties index 736fca4..693dfb0 100644 --- a/src/main/resources/config/roles/ jupyter-role-definition.properties +++ b/src/main/resources/config/roles/ jupyter-role-definition.properties @@ -11,5 +11,5 @@ org.nrg.Role=Jupyter org.nrg.Role.Jupyter.key=Jupyter org.nrg.Role.Jupyter.name=Jupyter Access -org.nrg.Role.Jupyter.warning=Users with access to Jupyter will be able to run arbitrary code from within their Jupyter containers. -org.nrg.Role.Jupyter.description=This is a custom user role for users who should be able to start Jupyter from various XNAT data types. Users given this role will be able to launch Jupyter notebooks containers and have read access to the data types they are launched from. This role is not required if the 'All Users Can Start Jupyter' plugin setting is set to true. +org.nrg.Role.Jupyter.warning=Users with access to Jupyter will be able to run arbitrary code from within their Jupyter servers. +org.nrg.Role.Jupyter.description=This is a custom user role for users who should be able to start Jupyter from various XNAT data types. Users given this role will be able to launch Jupyter servers and have read access to the data types they are launched from. This role is not required if the 'Enable Jupyter for all users' plugin setting is set to true. From 0939edd074ec11200b662f2febebfe1dff540593 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Wed, 24 May 2023 09:25:26 -0500 Subject: [PATCH 21/49] Bump to 1.0.0-RC1 --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 80ea344..acee24c 100644 --- a/build.gradle +++ b/build.gradle @@ -10,8 +10,8 @@ plugins { } group "org.nrg.xnatx.plugins" -version "1.0.0-SNAPSHOT" -description "JupyterHub Plugin for XNAT 1.8.6" +version "1.0.0-RC1" +description "JupyterHub Plugin for XNAT 1.8.8" repositories { maven { url "https://nrgxnat.jfrog.io/nrgxnat/libs-release" } @@ -28,7 +28,7 @@ configurations { } dependencies { - xnatProvided platform("org.nrg:parent:1.8.6") + xnatProvided platform("org.nrg:parent:1.8.8") xnatProvided "org.nrg:framework" xnatProvided "org.nrg.xnat:xnat-data-models" xnatProvided "org.nrg.xnat:web" From 4e3a686a2bead7c2daf6106e9611296041066b41 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Tue, 20 Jun 2023 13:53:19 -0500 Subject: [PATCH 22/49] JHP-60: Adds README - Adds README - Adds simple gradle task for deploying jar --- README.md | 35 +++++++++++++++-------------------- build.gradle | 6 ++++++ 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 39af52c..a66de90 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,24 @@ -# README # +# XNAT JupyterHub Plugin -This README would normally document whatever steps are necessary to get your application up and running. +[XNAT](https://www.xnat.org) plugin for integrating with [JupyterHub](https://jupyter.org/hub). -### What is this repository for? ### +See the [XNAT JupyterHub Plugin Wiki](https://wiki.xnat.org/jupyter-integration) for the latest documentation on this +plugin. -* Quick summary -* Version -* [Learn Markdown](https://bitbucket.org/tutorials/markdowndemo) +## Building the JAR -### How do I get set up? ### +To build the JAR, run the following command from the root of the project: -* Summary of set up -* Configuration -* Dependencies -* Database configuration -* How to run tests -* Deployment instructions +```bash +./gradlew clean jar +``` -### Contribution guidelines ### +The JAR will be built in the `build/libs` directory. -* Writing tests -* Code review -* Other guidelines +## Running the tests -### Who do I talk to? ### +To run the tests, run the following command from the root of the project: -* Repo owner or admin -* Other community or team contact \ No newline at end of file +```bash +./gradlew clean test +``` diff --git a/build.gradle b/build.gradle index acee24c..db08481 100644 --- a/build.gradle +++ b/build.gradle @@ -145,3 +145,9 @@ jar { } } } + +tasks.register('deploy-jar', Copy) { + description = "Copies the JAR file to a specified location. Requires the 'filePath' property to be set. Example: ./gradlew clean jar deploy-jar -PfilePath=/path/to/destination" + from jar + into project.findProperty('filePath') +} From b434c27e65fe575d3e365e765359b92e48d0f48f Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Thu, 22 Jun 2023 17:17:15 -0500 Subject: [PATCH 23/49] JHP-62: Rename job templates java package and javascript module name --- build.gradle | 8 +-- .../config/JobTemplatesConfig.java | 12 ----- .../xnat/compute/config/ComputeConfig.java | 12 +++++ .../entities/ComputeSpecConfigEntity.java | 4 +- .../compute}/entities/ComputeSpecEntity.java | 4 +- .../ComputeSpecHardwareOptionsEntity.java | 4 +- .../entities/ComputeSpecScopeEntity.java | 4 +- .../entities/ConstraintConfigEntity.java | 4 +- .../compute}/entities/ConstraintEntity.java | 9 ++-- .../entities/ConstraintScopeEntity.java | 4 +- .../entities/EnvironmentVariableEntity.java | 4 +- .../entities/GenericResourceEntity.java | 4 +- .../entities/HardwareConfigEntity.java | 4 +- .../entities/HardwareConstraintEntity.java | 4 +- .../compute}/entities/HardwareEntity.java | 4 +- .../entities/HardwareScopeEntity.java | 4 +- .../compute}/entities/MountEntity.java | 4 +- .../compute}/models/ComputeSpec.java | 7 ++- .../compute}/models/ComputeSpecConfig.java | 2 +- .../models/ComputeSpecHardwareOptions.java | 2 +- .../compute}/models/ComputeSpecScope.java | 2 +- .../compute}/models/Constraint.java | 2 +- .../compute}/models/ConstraintConfig.java | 2 +- .../compute}/models/ConstraintScope.java | 2 +- .../compute}/models/EnvironmentVariable.java | 2 +- .../compute}/models/GenericResource.java | 2 +- .../compute}/models/Hardware.java | 2 +- .../compute}/models/HardwareConfig.java | 2 +- .../compute}/models/HardwareScope.java | 2 +- .../compute}/models/JobTemplate.java | 2 +- .../compute}/models/Mount.java | 2 +- .../repositories/ComputeSpecConfigDao.java | 8 +-- .../repositories/ConstraintConfigDao.java | 8 +-- .../repositories/HardwareConfigDao.java | 7 ++- .../compute}/rest/ComputeSpecConfigsApi.java | 8 +-- .../compute}/rest/ConstraintConfigsApi.java | 8 +-- .../compute}/rest/HardwareConfigsApi.java | 8 +-- .../ComputeSpecConfigEntityService.java | 6 +-- .../services/ComputeSpecConfigService.java | 4 +- .../ConstraintConfigEntityService.java | 4 +- .../services/ConstraintConfigService.java | 4 +- .../services/HardwareConfigEntityService.java | 4 +- .../services/HardwareConfigService.java | 4 +- .../compute}/services/JobTemplateService.java | 4 +- .../impl/DefaultComputeSpecConfigService.java | 14 ++--- .../impl/DefaultConstraintConfigService.java | 14 ++--- .../impl/DefaultHardwareConfigService.java | 20 +++---- .../impl/DefaultJobTemplateService.java | 12 ++--- ...bernateComputeSpecConfigEntityService.java | 14 ++--- ...ibernateConstraintConfigEntityService.java | 8 +-- .../HibernateHardwareConfigEntityService.java | 8 +-- .../plugins/jupyterhub/JupyterHubPlugin.java | 10 ++-- ...HubEnvironmentsAndHardwareInitializer.java | 6 +-- .../impl/DefaultJupyterHubService.java | 2 +- .../impl/DefaultUserOptionsService.java | 6 +-- .../compute-spec-configs.js | 54 +++++++++---------- .../constraint-configs.js | 42 +++++++-------- .../hardware-configs.js | 42 +++++++-------- .../plugin/jupyterhub/jupyterhub-servers.js | 2 +- .../screens/topBar/Jupyter/Default.vm | 4 +- .../spawner/jupyterhub/site-settings.yaml | 14 ++--- src/main/resources/jupyterhub-logback.xml | 10 ++-- .../config/ComputeSpecConfigsApiConfig.java | 6 +-- .../ConstraintConfigsApiTestConfig.java | 6 +-- ...ultComputeSpecConfigServiceTestConfig.java | 8 +-- ...aultConstraintConfigServiceTestConfig.java | 6 +-- ...efaultHardwareConfigServiceTestConfig.java | 8 +-- .../DefaultJobTemplateServiceTestConfig.java | 10 ++-- .../config/HardwareConfigsApiConfig.java | 6 +-- ...puteSpecConfigEntityServiceTestConfig.java | 12 ++--- .../compute}/config/HibernateConfig.java | 4 +- ...nstraintConfigEntityServiceTestConfig.java | 8 +-- .../config/HibernateEntityServicesConfig.java | 20 +++---- ...HardwareConfigEntityServiceTestConfig.java | 8 +-- .../compute}/config/MockConfig.java | 8 +-- .../compute}/config/ObjectMapperConfig.java | 2 +- .../compute}/config/RestApiTestConfig.java | 2 +- .../compute}/models/ConstraintTest.java | 3 +- .../rest/ComputeSpecConfigsApiTest.java | 26 ++++----- .../rest/ConstraintConfigsApiTest.java | 22 ++++---- .../compute}/rest/HardwareConfigsApiTest.java | 22 ++++---- .../DefaultComputeSpecConfigServiceTest.java | 16 +++--- .../DefaultConstraintConfigServiceTest.java | 12 ++--- .../DefaultHardwareConfigServiceTest.java | 14 ++--- .../impl/DefaultJobTemplateServiceTest.java | 12 ++--- ...ateComputeSpecConfigEntityServiceTest.java | 20 +++---- ...nateConstraintConfigEntityServiceTest.java | 14 ++--- ...ernateHardwareConfigEntityServiceTest.java | 4 +- .../compute}/utils/TestingUtils.java | 2 +- .../DefaultJupyterHubServiceConfig.java | 2 +- .../DefaultUserOptionsServiceConfig.java | 2 +- ...mentsAndHardwareInitializerTestConfig.java | 4 +- .../plugins/jupyterhub/config/MockConfig.java | 6 +-- ...nvironmentsAndHardwareInitializerTest.java | 8 +-- .../impl/DefaultJupyterHubServiceTest.java | 2 +- .../impl/DefaultUserOptionsServiceTest.java | 2 +- 96 files changed, 398 insertions(+), 394 deletions(-) delete mode 100644 src/main/java/org/nrg/jobtemplates/config/JobTemplatesConfig.java create mode 100644 src/main/java/org/nrg/xnat/compute/config/ComputeConfig.java rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/ComputeSpecConfigEntity.java (98%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/ComputeSpecEntity.java (97%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/ComputeSpecHardwareOptionsEntity.java (96%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/ComputeSpecScopeEntity.java (96%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/ConstraintConfigEntity.java (97%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/ConstraintEntity.java (90%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/ConstraintScopeEntity.java (96%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/EnvironmentVariableEntity.java (92%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/GenericResourceEntity.java (92%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/HardwareConfigEntity.java (97%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/HardwareConstraintEntity.java (95%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/HardwareEntity.java (98%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/HardwareScopeEntity.java (96%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/entities/MountEntity.java (95%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/ComputeSpec.java (76%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/ComputeSpecConfig.java (95%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/ComputeSpecHardwareOptions.java (89%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/ComputeSpecScope.java (93%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/Constraint.java (96%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/ConstraintConfig.java (93%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/ConstraintScope.java (93%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/EnvironmentVariable.java (87%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/GenericResource.java (87%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/Hardware.java (95%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/HardwareConfig.java (93%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/HardwareScope.java (93%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/JobTemplate.java (89%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/models/Mount.java (89%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/repositories/ComputeSpecConfigDao.java (94%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/repositories/ConstraintConfigDao.java (88%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/repositories/HardwareConfigDao.java (89%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/rest/ComputeSpecConfigsApi.java (97%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/rest/ConstraintConfigsApi.java (96%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/rest/HardwareConfigsApi.java (96%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/ComputeSpecConfigEntityService.java (74%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/ComputeSpecConfigService.java (89%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/ConstraintConfigEntityService.java (62%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/ConstraintConfigService.java (83%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/HardwareConfigEntityService.java (62%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/HardwareConfigService.java (85%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/JobTemplateService.java (74%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/DefaultComputeSpecConfigService.java (97%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/DefaultConstraintConfigService.java (94%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/DefaultHardwareConfigService.java (93%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/DefaultJobTemplateService.java (93%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/HibernateComputeSpecConfigEntityService.java (87%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/HibernateConstraintConfigEntityService.java (60%) rename src/main/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/HibernateHardwareConfigEntityService.java (60%) rename src/main/resources/META-INF/resources/scripts/xnat/plugin/{jobTemplates => compute}/compute-spec-configs.js (94%) rename src/main/resources/META-INF/resources/scripts/xnat/plugin/{jobTemplates => compute}/constraint-configs.js (93%) rename src/main/resources/META-INF/resources/scripts/xnat/plugin/{jobTemplates => compute}/hardware-configs.js (96%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/ComputeSpecConfigsApiConfig.java (92%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/ConstraintConfigsApiTestConfig.java (92%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/DefaultComputeSpecConfigServiceTestConfig.java (76%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/DefaultConstraintConfigServiceTestConfig.java (77%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/DefaultHardwareConfigServiceTestConfig.java (76%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/DefaultJobTemplateServiceTestConfig.java (74%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/HardwareConfigsApiConfig.java (92%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/HibernateComputeSpecConfigEntityServiceTestConfig.java (90%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/HibernateConfig.java (97%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/HibernateConstraintConfigEntityServiceTestConfig.java (91%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/HibernateEntityServicesConfig.java (76%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/HibernateHardwareConfigEntityServiceTestConfig.java (91%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/MockConfig.java (94%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/ObjectMapperConfig.java (97%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/config/RestApiTestConfig.java (98%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/models/ConstraintTest.java (92%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/rest/ComputeSpecConfigsApiTest.java (91%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/rest/ConstraintConfigsApiTest.java (92%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/rest/HardwareConfigsApiTest.java (92%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/DefaultComputeSpecConfigServiceTest.java (98%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/DefaultConstraintConfigServiceTest.java (96%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/DefaultHardwareConfigServiceTest.java (97%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/DefaultJobTemplateServiceTest.java (95%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/HibernateComputeSpecConfigEntityServiceTest.java (97%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/HibernateConstraintConfigEntityServiceTest.java (88%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/services/impl/HibernateHardwareConfigEntityServiceTest.java (86%) rename src/test/java/org/nrg/{jobtemplates => xnat/compute}/utils/TestingUtils.java (89%) diff --git a/build.gradle b/build.gradle index db08481..b1da4ca 100644 --- a/build.gradle +++ b/build.gradle @@ -3,15 +3,15 @@ */ plugins { id "java" - id "org.nrg.xnat.build.xnat-data-builder" version "1.8.6" + id "org.nrg.xnat.build.xnat-data-builder" version "1.8.8" id "io.freefair.lombok" version "6.0.0-m2" id "com.palantir.git-version" version "0.12.1" id "jacoco" } group "org.nrg.xnatx.plugins" -version "1.0.0-RC1" -description "JupyterHub Plugin for XNAT 1.8.8" +version "1.0.0-RC2-SNAPSHOT" +description "JupyterHub Plugin for XNAT" repositories { maven { url "https://nrgxnat.jfrog.io/nrgxnat/libs-release" } @@ -28,7 +28,7 @@ configurations { } dependencies { - xnatProvided platform("org.nrg:parent:1.8.8") + xnatProvided platform("org.nrg:parent:1.8.8.1") xnatProvided "org.nrg:framework" xnatProvided "org.nrg.xnat:xnat-data-models" xnatProvided "org.nrg.xnat:web" diff --git a/src/main/java/org/nrg/jobtemplates/config/JobTemplatesConfig.java b/src/main/java/org/nrg/jobtemplates/config/JobTemplatesConfig.java deleted file mode 100644 index 96a8854..0000000 --- a/src/main/java/org/nrg/jobtemplates/config/JobTemplatesConfig.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.nrg.jobtemplates.config; - -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; - -@Configuration -@ComponentScan({"org.nrg.jobtemplates.services.impl", - "org.nrg.jobtemplates.services", - "org.nrg.jobtemplates.rest", - "org.nrg.jobtemplates.repositories"}) -public class JobTemplatesConfig { -} diff --git a/src/main/java/org/nrg/xnat/compute/config/ComputeConfig.java b/src/main/java/org/nrg/xnat/compute/config/ComputeConfig.java new file mode 100644 index 0000000..4cf6877 --- /dev/null +++ b/src/main/java/org/nrg/xnat/compute/config/ComputeConfig.java @@ -0,0 +1,12 @@ +package org.nrg.xnat.compute.config; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan({"org.nrg.xnat.compute.services.impl", + "org.nrg.xnat.compute.services", + "org.nrg.xnat.compute.rest", + "org.nrg.xnat.compute.repositories"}) +public class ComputeConfig { +} diff --git a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecConfigEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecConfigEntity.java similarity index 98% rename from src/main/java/org/nrg/jobtemplates/entities/ComputeSpecConfigEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/ComputeSpecConfigEntity.java index 0f66407..6f30f80 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecConfigEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecConfigEntity.java @@ -1,9 +1,9 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; import org.nrg.framework.constants.Scope; import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.jobtemplates.models.ComputeSpecConfig; +import org.nrg.xnat.compute.models.ComputeSpecConfig; import javax.persistence.*; import java.util.HashSet; diff --git a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecEntity.java similarity index 97% rename from src/main/java/org/nrg/jobtemplates/entities/ComputeSpecEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/ComputeSpecEntity.java index a05f589..5c094ea 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecEntity.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.jobtemplates.models.ComputeSpec; +import org.nrg.xnat.compute.models.ComputeSpec; import javax.persistence.*; import java.util.ArrayList; diff --git a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecHardwareOptionsEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecHardwareOptionsEntity.java similarity index 96% rename from src/main/java/org/nrg/jobtemplates/entities/ComputeSpecHardwareOptionsEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/ComputeSpecHardwareOptionsEntity.java index 2f25db9..b7e34b0 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecHardwareOptionsEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecHardwareOptionsEntity.java @@ -1,7 +1,7 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; -import org.nrg.jobtemplates.models.ComputeSpecHardwareOptions; +import org.nrg.xnat.compute.models.ComputeSpecHardwareOptions; import javax.persistence.*; import java.util.HashSet; diff --git a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecScopeEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecScopeEntity.java similarity index 96% rename from src/main/java/org/nrg/jobtemplates/entities/ComputeSpecScopeEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/ComputeSpecScopeEntity.java index 476ea4e..4facacb 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/ComputeSpecScopeEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecScopeEntity.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; import org.nrg.framework.constants.Scope; -import org.nrg.jobtemplates.models.ComputeSpecScope; +import org.nrg.xnat.compute.models.ComputeSpecScope; import javax.persistence.*; import java.util.HashSet; diff --git a/src/main/java/org/nrg/jobtemplates/entities/ConstraintConfigEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ConstraintConfigEntity.java similarity index 97% rename from src/main/java/org/nrg/jobtemplates/entities/ConstraintConfigEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/ConstraintConfigEntity.java index 2bb5d74..09436da 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/ConstraintConfigEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/ConstraintConfigEntity.java @@ -1,9 +1,9 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; import org.nrg.framework.constants.Scope; import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.jobtemplates.models.ConstraintConfig; +import org.nrg.xnat.compute.models.ConstraintConfig; import javax.persistence.CascadeType; import javax.persistence.Entity; diff --git a/src/main/java/org/nrg/jobtemplates/entities/ConstraintEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ConstraintEntity.java similarity index 90% rename from src/main/java/org/nrg/jobtemplates/entities/ConstraintEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/ConstraintEntity.java index b4f1f40..e621c93 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/ConstraintEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/ConstraintEntity.java @@ -1,10 +1,13 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.jobtemplates.models.Constraint; +import org.nrg.xnat.compute.models.Constraint; -import javax.persistence.*; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.MapsId; +import javax.persistence.OneToOne; import java.util.HashSet; import java.util.Set; diff --git a/src/main/java/org/nrg/jobtemplates/entities/ConstraintScopeEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ConstraintScopeEntity.java similarity index 96% rename from src/main/java/org/nrg/jobtemplates/entities/ConstraintScopeEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/ConstraintScopeEntity.java index a9cc1fd..05cd6b9 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/ConstraintScopeEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/ConstraintScopeEntity.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; import org.nrg.framework.constants.Scope; -import org.nrg.jobtemplates.models.ConstraintScope; +import org.nrg.xnat.compute.models.ConstraintScope; import javax.persistence.*; import java.util.HashSet; diff --git a/src/main/java/org/nrg/jobtemplates/entities/EnvironmentVariableEntity.java b/src/main/java/org/nrg/xnat/compute/entities/EnvironmentVariableEntity.java similarity index 92% rename from src/main/java/org/nrg/jobtemplates/entities/EnvironmentVariableEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/EnvironmentVariableEntity.java index 896b0c8..84f7dd4 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/EnvironmentVariableEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/EnvironmentVariableEntity.java @@ -1,7 +1,7 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; -import org.nrg.jobtemplates.models.EnvironmentVariable; +import org.nrg.xnat.compute.models.EnvironmentVariable; import javax.persistence.Embeddable; diff --git a/src/main/java/org/nrg/jobtemplates/entities/GenericResourceEntity.java b/src/main/java/org/nrg/xnat/compute/entities/GenericResourceEntity.java similarity index 92% rename from src/main/java/org/nrg/jobtemplates/entities/GenericResourceEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/GenericResourceEntity.java index bcd2109..a147093 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/GenericResourceEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/GenericResourceEntity.java @@ -1,7 +1,7 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; -import org.nrg.jobtemplates.models.GenericResource; +import org.nrg.xnat.compute.models.GenericResource; import javax.persistence.Embeddable; diff --git a/src/main/java/org/nrg/jobtemplates/entities/HardwareConfigEntity.java b/src/main/java/org/nrg/xnat/compute/entities/HardwareConfigEntity.java similarity index 97% rename from src/main/java/org/nrg/jobtemplates/entities/HardwareConfigEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/HardwareConfigEntity.java index ab27f0d..75c37c0 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/HardwareConfigEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/HardwareConfigEntity.java @@ -1,9 +1,9 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; import org.nrg.framework.constants.Scope; import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.jobtemplates.models.HardwareConfig; +import org.nrg.xnat.compute.models.HardwareConfig; import javax.persistence.*; import java.util.List; diff --git a/src/main/java/org/nrg/jobtemplates/entities/HardwareConstraintEntity.java b/src/main/java/org/nrg/xnat/compute/entities/HardwareConstraintEntity.java similarity index 95% rename from src/main/java/org/nrg/jobtemplates/entities/HardwareConstraintEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/HardwareConstraintEntity.java index a0875e3..10a7c86 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/HardwareConstraintEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/HardwareConstraintEntity.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.jobtemplates.models.Constraint; +import org.nrg.xnat.compute.models.Constraint; import javax.persistence.*; import java.util.Set; diff --git a/src/main/java/org/nrg/jobtemplates/entities/HardwareEntity.java b/src/main/java/org/nrg/xnat/compute/entities/HardwareEntity.java similarity index 98% rename from src/main/java/org/nrg/jobtemplates/entities/HardwareEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/HardwareEntity.java index c8c31d4..22ee0a7 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/HardwareEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/HardwareEntity.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.jobtemplates.models.Hardware; +import org.nrg.xnat.compute.models.Hardware; import javax.persistence.*; import java.util.ArrayList; diff --git a/src/main/java/org/nrg/jobtemplates/entities/HardwareScopeEntity.java b/src/main/java/org/nrg/xnat/compute/entities/HardwareScopeEntity.java similarity index 96% rename from src/main/java/org/nrg/jobtemplates/entities/HardwareScopeEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/HardwareScopeEntity.java index 00af3ad..47c3e2d 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/HardwareScopeEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/HardwareScopeEntity.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; import org.nrg.framework.constants.Scope; -import org.nrg.jobtemplates.models.HardwareScope; +import org.nrg.xnat.compute.models.HardwareScope; import javax.persistence.*; import java.util.HashSet; diff --git a/src/main/java/org/nrg/jobtemplates/entities/MountEntity.java b/src/main/java/org/nrg/xnat/compute/entities/MountEntity.java similarity index 95% rename from src/main/java/org/nrg/jobtemplates/entities/MountEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/MountEntity.java index 25d6647..2c5d9f2 100644 --- a/src/main/java/org/nrg/jobtemplates/entities/MountEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/MountEntity.java @@ -1,7 +1,7 @@ -package org.nrg.jobtemplates.entities; +package org.nrg.xnat.compute.entities; import lombok.*; -import org.nrg.jobtemplates.models.Mount; +import org.nrg.xnat.compute.models.Mount; import javax.persistence.Embeddable; diff --git a/src/main/java/org/nrg/jobtemplates/models/ComputeSpec.java b/src/main/java/org/nrg/xnat/compute/models/ComputeSpec.java similarity index 76% rename from src/main/java/org/nrg/jobtemplates/models/ComputeSpec.java rename to src/main/java/org/nrg/xnat/compute/models/ComputeSpec.java index 15bcda1..30acca7 100644 --- a/src/main/java/org/nrg/jobtemplates/models/ComputeSpec.java +++ b/src/main/java/org/nrg/xnat/compute/models/ComputeSpec.java @@ -1,7 +1,10 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import io.swagger.annotations.ApiModelProperty; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; diff --git a/src/main/java/org/nrg/jobtemplates/models/ComputeSpecConfig.java b/src/main/java/org/nrg/xnat/compute/models/ComputeSpecConfig.java similarity index 95% rename from src/main/java/org/nrg/jobtemplates/models/ComputeSpecConfig.java rename to src/main/java/org/nrg/xnat/compute/models/ComputeSpecConfig.java index c10ac11..631402c 100644 --- a/src/main/java/org/nrg/jobtemplates/models/ComputeSpecConfig.java +++ b/src/main/java/org/nrg/xnat/compute/models/ComputeSpecConfig.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; diff --git a/src/main/java/org/nrg/jobtemplates/models/ComputeSpecHardwareOptions.java b/src/main/java/org/nrg/xnat/compute/models/ComputeSpecHardwareOptions.java similarity index 89% rename from src/main/java/org/nrg/jobtemplates/models/ComputeSpecHardwareOptions.java rename to src/main/java/org/nrg/xnat/compute/models/ComputeSpecHardwareOptions.java index 2c21f9a..8a80f26 100644 --- a/src/main/java/org/nrg/jobtemplates/models/ComputeSpecHardwareOptions.java +++ b/src/main/java/org/nrg/xnat/compute/models/ComputeSpecHardwareOptions.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/nrg/jobtemplates/models/ComputeSpecScope.java b/src/main/java/org/nrg/xnat/compute/models/ComputeSpecScope.java similarity index 93% rename from src/main/java/org/nrg/jobtemplates/models/ComputeSpecScope.java rename to src/main/java/org/nrg/xnat/compute/models/ComputeSpecScope.java index 146c414..880f1c8 100644 --- a/src/main/java/org/nrg/jobtemplates/models/ComputeSpecScope.java +++ b/src/main/java/org/nrg/xnat/compute/models/ComputeSpecScope.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/nrg/jobtemplates/models/Constraint.java b/src/main/java/org/nrg/xnat/compute/models/Constraint.java similarity index 96% rename from src/main/java/org/nrg/jobtemplates/models/Constraint.java rename to src/main/java/org/nrg/xnat/compute/models/Constraint.java index 31f4e97..1e27938 100644 --- a/src/main/java/org/nrg/jobtemplates/models/Constraint.java +++ b/src/main/java/org/nrg/xnat/compute/models/Constraint.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/nrg/jobtemplates/models/ConstraintConfig.java b/src/main/java/org/nrg/xnat/compute/models/ConstraintConfig.java similarity index 93% rename from src/main/java/org/nrg/jobtemplates/models/ConstraintConfig.java rename to src/main/java/org/nrg/xnat/compute/models/ConstraintConfig.java index 66ea874..9fbcbe2 100644 --- a/src/main/java/org/nrg/jobtemplates/models/ConstraintConfig.java +++ b/src/main/java/org/nrg/xnat/compute/models/ConstraintConfig.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; diff --git a/src/main/java/org/nrg/jobtemplates/models/ConstraintScope.java b/src/main/java/org/nrg/xnat/compute/models/ConstraintScope.java similarity index 93% rename from src/main/java/org/nrg/jobtemplates/models/ConstraintScope.java rename to src/main/java/org/nrg/xnat/compute/models/ConstraintScope.java index 9316dfd..ed5cccb 100644 --- a/src/main/java/org/nrg/jobtemplates/models/ConstraintScope.java +++ b/src/main/java/org/nrg/xnat/compute/models/ConstraintScope.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/nrg/jobtemplates/models/EnvironmentVariable.java b/src/main/java/org/nrg/xnat/compute/models/EnvironmentVariable.java similarity index 87% rename from src/main/java/org/nrg/jobtemplates/models/EnvironmentVariable.java rename to src/main/java/org/nrg/xnat/compute/models/EnvironmentVariable.java index 08f80e1..d605fa6 100644 --- a/src/main/java/org/nrg/jobtemplates/models/EnvironmentVariable.java +++ b/src/main/java/org/nrg/xnat/compute/models/EnvironmentVariable.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/nrg/jobtemplates/models/GenericResource.java b/src/main/java/org/nrg/xnat/compute/models/GenericResource.java similarity index 87% rename from src/main/java/org/nrg/jobtemplates/models/GenericResource.java rename to src/main/java/org/nrg/xnat/compute/models/GenericResource.java index 7ac5a9b..c2a6995 100644 --- a/src/main/java/org/nrg/jobtemplates/models/GenericResource.java +++ b/src/main/java/org/nrg/xnat/compute/models/GenericResource.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/nrg/jobtemplates/models/Hardware.java b/src/main/java/org/nrg/xnat/compute/models/Hardware.java similarity index 95% rename from src/main/java/org/nrg/jobtemplates/models/Hardware.java rename to src/main/java/org/nrg/xnat/compute/models/Hardware.java index ddba1d1..a68e442 100644 --- a/src/main/java/org/nrg/jobtemplates/models/Hardware.java +++ b/src/main/java/org/nrg/xnat/compute/models/Hardware.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; diff --git a/src/main/java/org/nrg/jobtemplates/models/HardwareConfig.java b/src/main/java/org/nrg/xnat/compute/models/HardwareConfig.java similarity index 93% rename from src/main/java/org/nrg/jobtemplates/models/HardwareConfig.java rename to src/main/java/org/nrg/xnat/compute/models/HardwareConfig.java index e7c1c60..c9eaf68 100644 --- a/src/main/java/org/nrg/jobtemplates/models/HardwareConfig.java +++ b/src/main/java/org/nrg/xnat/compute/models/HardwareConfig.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; diff --git a/src/main/java/org/nrg/jobtemplates/models/HardwareScope.java b/src/main/java/org/nrg/xnat/compute/models/HardwareScope.java similarity index 93% rename from src/main/java/org/nrg/jobtemplates/models/HardwareScope.java rename to src/main/java/org/nrg/xnat/compute/models/HardwareScope.java index 81a1934..e583c69 100644 --- a/src/main/java/org/nrg/jobtemplates/models/HardwareScope.java +++ b/src/main/java/org/nrg/xnat/compute/models/HardwareScope.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/nrg/jobtemplates/models/JobTemplate.java b/src/main/java/org/nrg/xnat/compute/models/JobTemplate.java similarity index 89% rename from src/main/java/org/nrg/jobtemplates/models/JobTemplate.java rename to src/main/java/org/nrg/xnat/compute/models/JobTemplate.java index ace6fdf..a6a3a64 100644 --- a/src/main/java/org/nrg/jobtemplates/models/JobTemplate.java +++ b/src/main/java/org/nrg/xnat/compute/models/JobTemplate.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/nrg/jobtemplates/models/Mount.java b/src/main/java/org/nrg/xnat/compute/models/Mount.java similarity index 89% rename from src/main/java/org/nrg/jobtemplates/models/Mount.java rename to src/main/java/org/nrg/xnat/compute/models/Mount.java index dedd5fc..8c3c15c 100644 --- a/src/main/java/org/nrg/jobtemplates/models/Mount.java +++ b/src/main/java/org/nrg/xnat/compute/models/Mount.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/nrg/jobtemplates/repositories/ComputeSpecConfigDao.java b/src/main/java/org/nrg/xnat/compute/repositories/ComputeSpecConfigDao.java similarity index 94% rename from src/main/java/org/nrg/jobtemplates/repositories/ComputeSpecConfigDao.java rename to src/main/java/org/nrg/xnat/compute/repositories/ComputeSpecConfigDao.java index 67520ca..a9d8bcf 100644 --- a/src/main/java/org/nrg/jobtemplates/repositories/ComputeSpecConfigDao.java +++ b/src/main/java/org/nrg/xnat/compute/repositories/ComputeSpecConfigDao.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.repositories; +package org.nrg.xnat.compute.repositories; import lombok.extern.slf4j.Slf4j; import org.hibernate.Criteria; @@ -6,9 +6,9 @@ import org.hibernate.SessionFactory; import org.hibernate.criterion.Restrictions; import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; -import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; -import org.nrg.jobtemplates.entities.ComputeSpecScopeEntity; -import org.nrg.jobtemplates.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; +import org.nrg.xnat.compute.entities.ComputeSpecScopeEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; diff --git a/src/main/java/org/nrg/jobtemplates/repositories/ConstraintConfigDao.java b/src/main/java/org/nrg/xnat/compute/repositories/ConstraintConfigDao.java similarity index 88% rename from src/main/java/org/nrg/jobtemplates/repositories/ConstraintConfigDao.java rename to src/main/java/org/nrg/xnat/compute/repositories/ConstraintConfigDao.java index 966249c..46ccb97 100644 --- a/src/main/java/org/nrg/jobtemplates/repositories/ConstraintConfigDao.java +++ b/src/main/java/org/nrg/xnat/compute/repositories/ConstraintConfigDao.java @@ -1,12 +1,12 @@ -package org.nrg.jobtemplates.repositories; +package org.nrg.xnat.compute.repositories; import lombok.extern.slf4j.Slf4j; import org.hibernate.Hibernate; import org.hibernate.SessionFactory; import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; -import org.nrg.jobtemplates.entities.ConstraintConfigEntity; -import org.nrg.jobtemplates.entities.ConstraintEntity; -import org.nrg.jobtemplates.entities.ConstraintScopeEntity; +import org.nrg.xnat.compute.entities.ConstraintConfigEntity; +import org.nrg.xnat.compute.entities.ConstraintEntity; +import org.nrg.xnat.compute.entities.ConstraintScopeEntity; import org.springframework.stereotype.Repository; @Repository diff --git a/src/main/java/org/nrg/jobtemplates/repositories/HardwareConfigDao.java b/src/main/java/org/nrg/xnat/compute/repositories/HardwareConfigDao.java similarity index 89% rename from src/main/java/org/nrg/jobtemplates/repositories/HardwareConfigDao.java rename to src/main/java/org/nrg/xnat/compute/repositories/HardwareConfigDao.java index 0cf67b2..a81ce5d 100644 --- a/src/main/java/org/nrg/jobtemplates/repositories/HardwareConfigDao.java +++ b/src/main/java/org/nrg/xnat/compute/repositories/HardwareConfigDao.java @@ -1,10 +1,13 @@ -package org.nrg.jobtemplates.repositories; +package org.nrg.xnat.compute.repositories; import lombok.extern.slf4j.Slf4j; import org.hibernate.Hibernate; import org.hibernate.SessionFactory; import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; -import org.nrg.jobtemplates.entities.*; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.entities.HardwareConstraintEntity; +import org.nrg.xnat.compute.entities.HardwareEntity; +import org.nrg.xnat.compute.entities.HardwareScopeEntity; import org.springframework.stereotype.Repository; @Repository diff --git a/src/main/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApi.java b/src/main/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApi.java similarity index 97% rename from src/main/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApi.java rename to src/main/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApi.java index cdba6a3..8c16adf 100644 --- a/src/main/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApi.java +++ b/src/main/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApi.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.rest; +package org.nrg.xnat.compute.rest; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -6,12 +6,12 @@ import io.swagger.annotations.ApiResponses; import org.nrg.framework.annotations.XapiRestController; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.jobtemplates.models.ComputeSpecConfig; -import org.nrg.jobtemplates.services.ComputeSpecConfigService; import org.nrg.xapi.rest.AbstractXapiRestController; import org.nrg.xapi.rest.XapiRequestMapping; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnat.compute.models.ComputeSpecConfig; +import org.nrg.xnat.compute.services.ComputeSpecConfigService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -24,7 +24,7 @@ @Api("ComputeSpecConfigs REST API") @XapiRestController -@RequestMapping(value = "/job-templates/compute-spec-configs") +@RequestMapping(value = "/compute-spec-configs") public class ComputeSpecConfigsApi extends AbstractXapiRestController { private final ComputeSpecConfigService computeSpecConfigService; diff --git a/src/main/java/org/nrg/jobtemplates/rest/ConstraintConfigsApi.java b/src/main/java/org/nrg/xnat/compute/rest/ConstraintConfigsApi.java similarity index 96% rename from src/main/java/org/nrg/jobtemplates/rest/ConstraintConfigsApi.java rename to src/main/java/org/nrg/xnat/compute/rest/ConstraintConfigsApi.java index 25edc4e..50b711d 100644 --- a/src/main/java/org/nrg/jobtemplates/rest/ConstraintConfigsApi.java +++ b/src/main/java/org/nrg/xnat/compute/rest/ConstraintConfigsApi.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.rest; +package org.nrg.xnat.compute.rest; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -6,12 +6,12 @@ import io.swagger.annotations.ApiResponses; import org.nrg.framework.annotations.XapiRestController; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.jobtemplates.models.ConstraintConfig; -import org.nrg.jobtemplates.services.ConstraintConfigService; import org.nrg.xapi.rest.AbstractXapiRestController; import org.nrg.xapi.rest.XapiRequestMapping; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnat.compute.models.ConstraintConfig; +import org.nrg.xnat.compute.services.ConstraintConfigService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PathVariable; @@ -28,7 +28,7 @@ @Api("Constraint Configs REST API") @XapiRestController -@RequestMapping(value = "/job-templates/constraint-configs") +@RequestMapping(value = "/constraint-configs") public class ConstraintConfigsApi extends AbstractXapiRestController { private final ConstraintConfigService constraintConfigService; diff --git a/src/main/java/org/nrg/jobtemplates/rest/HardwareConfigsApi.java b/src/main/java/org/nrg/xnat/compute/rest/HardwareConfigsApi.java similarity index 96% rename from src/main/java/org/nrg/jobtemplates/rest/HardwareConfigsApi.java rename to src/main/java/org/nrg/xnat/compute/rest/HardwareConfigsApi.java index c76bf88..60af77e 100644 --- a/src/main/java/org/nrg/jobtemplates/rest/HardwareConfigsApi.java +++ b/src/main/java/org/nrg/xnat/compute/rest/HardwareConfigsApi.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.rest; +package org.nrg.xnat.compute.rest; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -6,12 +6,12 @@ import io.swagger.annotations.ApiResponses; import org.nrg.framework.annotations.XapiRestController; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.jobtemplates.models.HardwareConfig; -import org.nrg.jobtemplates.services.HardwareConfigService; import org.nrg.xapi.rest.AbstractXapiRestController; import org.nrg.xapi.rest.XapiRequestMapping; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnat.compute.models.HardwareConfig; +import org.nrg.xnat.compute.services.HardwareConfigService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PathVariable; @@ -27,7 +27,7 @@ @Api("Hardware Configs REST API") @XapiRestController -@RequestMapping(value = "/job-templates/hardware-configs") +@RequestMapping(value = "/hardware-configs") public class HardwareConfigsApi extends AbstractXapiRestController { private final HardwareConfigService hardwareConfigService; diff --git a/src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigEntityService.java similarity index 74% rename from src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigEntityService.java rename to src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigEntityService.java index 1ffeb07..021985c 100644 --- a/src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigEntityService.java +++ b/src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigEntityService.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.services; +package org.nrg.xnat.compute.services; import org.nrg.framework.orm.hibernate.BaseHibernateService; -import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; -import org.nrg.jobtemplates.models.ComputeSpecConfig; +import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; +import org.nrg.xnat.compute.models.ComputeSpecConfig; import java.util.List; diff --git a/src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigService.java b/src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigService.java similarity index 89% rename from src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigService.java rename to src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigService.java index 1525841..9c547c3 100644 --- a/src/main/java/org/nrg/jobtemplates/services/ComputeSpecConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigService.java @@ -1,7 +1,7 @@ -package org.nrg.jobtemplates.services; +package org.nrg.xnat.compute.services; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.jobtemplates.models.ComputeSpecConfig; +import org.nrg.xnat.compute.models.ComputeSpecConfig; import java.util.List; import java.util.Optional; diff --git a/src/main/java/org/nrg/jobtemplates/services/ConstraintConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigEntityService.java similarity index 62% rename from src/main/java/org/nrg/jobtemplates/services/ConstraintConfigEntityService.java rename to src/main/java/org/nrg/xnat/compute/services/ConstraintConfigEntityService.java index a1db025..4adcd2b 100644 --- a/src/main/java/org/nrg/jobtemplates/services/ConstraintConfigEntityService.java +++ b/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigEntityService.java @@ -1,7 +1,7 @@ -package org.nrg.jobtemplates.services; +package org.nrg.xnat.compute.services; import org.nrg.framework.orm.hibernate.BaseHibernateService; -import org.nrg.jobtemplates.entities.ConstraintConfigEntity; +import org.nrg.xnat.compute.entities.ConstraintConfigEntity; public interface ConstraintConfigEntityService extends BaseHibernateService { diff --git a/src/main/java/org/nrg/jobtemplates/services/ConstraintConfigService.java b/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigService.java similarity index 83% rename from src/main/java/org/nrg/jobtemplates/services/ConstraintConfigService.java rename to src/main/java/org/nrg/xnat/compute/services/ConstraintConfigService.java index 43a02c4..bbb2648 100644 --- a/src/main/java/org/nrg/jobtemplates/services/ConstraintConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigService.java @@ -1,7 +1,7 @@ -package org.nrg.jobtemplates.services; +package org.nrg.xnat.compute.services; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.jobtemplates.models.ConstraintConfig; +import org.nrg.xnat.compute.models.ConstraintConfig; import java.util.List; import java.util.Optional; diff --git a/src/main/java/org/nrg/jobtemplates/services/HardwareConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/HardwareConfigEntityService.java similarity index 62% rename from src/main/java/org/nrg/jobtemplates/services/HardwareConfigEntityService.java rename to src/main/java/org/nrg/xnat/compute/services/HardwareConfigEntityService.java index 47fcb74..de8aa74 100644 --- a/src/main/java/org/nrg/jobtemplates/services/HardwareConfigEntityService.java +++ b/src/main/java/org/nrg/xnat/compute/services/HardwareConfigEntityService.java @@ -1,7 +1,7 @@ -package org.nrg.jobtemplates.services; +package org.nrg.xnat.compute.services; import org.nrg.framework.orm.hibernate.BaseHibernateService; -import org.nrg.jobtemplates.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; public interface HardwareConfigEntityService extends BaseHibernateService { diff --git a/src/main/java/org/nrg/jobtemplates/services/HardwareConfigService.java b/src/main/java/org/nrg/xnat/compute/services/HardwareConfigService.java similarity index 85% rename from src/main/java/org/nrg/jobtemplates/services/HardwareConfigService.java rename to src/main/java/org/nrg/xnat/compute/services/HardwareConfigService.java index aba3249..3a1d2a9 100644 --- a/src/main/java/org/nrg/jobtemplates/services/HardwareConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/HardwareConfigService.java @@ -1,7 +1,7 @@ -package org.nrg.jobtemplates.services; +package org.nrg.xnat.compute.services; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.jobtemplates.models.HardwareConfig; +import org.nrg.xnat.compute.models.HardwareConfig; import java.util.List; import java.util.Optional; diff --git a/src/main/java/org/nrg/jobtemplates/services/JobTemplateService.java b/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java similarity index 74% rename from src/main/java/org/nrg/jobtemplates/services/JobTemplateService.java rename to src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java index 7fc9023..6155d95 100644 --- a/src/main/java/org/nrg/jobtemplates/services/JobTemplateService.java +++ b/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java @@ -1,6 +1,6 @@ -package org.nrg.jobtemplates.services; +package org.nrg.xnat.compute.services; -import org.nrg.jobtemplates.models.JobTemplate; +import org.nrg.xnat.compute.models.JobTemplate; public interface JobTemplateService { diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigService.java similarity index 97% rename from src/main/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigService.java rename to src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigService.java index 92c9fb5..1705aeb 100644 --- a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigService.java @@ -1,15 +1,15 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; -import org.nrg.jobtemplates.entities.HardwareConfigEntity; -import org.nrg.jobtemplates.models.*; -import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; -import org.nrg.jobtemplates.services.ComputeSpecConfigService; -import org.nrg.jobtemplates.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.models.*; +import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigService.java similarity index 94% rename from src/main/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigService.java rename to src/main/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigService.java index dae670a..205700e 100644 --- a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigService.java @@ -1,15 +1,15 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.jobtemplates.entities.ConstraintConfigEntity; -import org.nrg.jobtemplates.models.Constraint; -import org.nrg.jobtemplates.models.ConstraintConfig; -import org.nrg.jobtemplates.models.ConstraintScope; -import org.nrg.jobtemplates.services.ConstraintConfigEntityService; -import org.nrg.jobtemplates.services.ConstraintConfigService; +import org.nrg.xnat.compute.entities.ConstraintConfigEntity; +import org.nrg.xnat.compute.models.Constraint; +import org.nrg.xnat.compute.models.ConstraintConfig; +import org.nrg.xnat.compute.models.ConstraintScope; +import org.nrg.xnat.compute.services.ConstraintConfigEntityService; +import org.nrg.xnat.compute.services.ConstraintConfigService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java similarity index 93% rename from src/main/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigService.java rename to src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java index 2bfa06b..8b7df1e 100644 --- a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java @@ -1,18 +1,18 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; -import org.nrg.jobtemplates.entities.ComputeSpecHardwareOptionsEntity; -import org.nrg.jobtemplates.entities.HardwareConfigEntity; -import org.nrg.jobtemplates.models.Hardware; -import org.nrg.jobtemplates.models.HardwareConfig; -import org.nrg.jobtemplates.models.HardwareScope; -import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; -import org.nrg.jobtemplates.services.HardwareConfigEntityService; -import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; +import org.nrg.xnat.compute.entities.ComputeSpecHardwareOptionsEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.models.Hardware; +import org.nrg.xnat.compute.models.HardwareConfig; +import org.nrg.xnat.compute.models.HardwareScope; +import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java similarity index 93% rename from src/main/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateService.java rename to src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java index a502508..eaabec0 100644 --- a/src/main/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java @@ -1,11 +1,11 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import lombok.extern.slf4j.Slf4j; -import org.nrg.jobtemplates.models.*; -import org.nrg.jobtemplates.services.ComputeSpecConfigService; -import org.nrg.jobtemplates.services.ConstraintConfigService; -import org.nrg.jobtemplates.services.HardwareConfigService; -import org.nrg.jobtemplates.services.JobTemplateService; +import org.nrg.xnat.compute.models.*; +import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.ConstraintConfigService; +import org.nrg.xnat.compute.services.HardwareConfigService; +import org.nrg.xnat.compute.services.JobTemplateService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityService.java similarity index 87% rename from src/main/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityService.java rename to src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityService.java index 29e5b50..9cd7ef6 100644 --- a/src/main/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityService.java @@ -1,13 +1,13 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import lombok.extern.slf4j.Slf4j; import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; -import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; -import org.nrg.jobtemplates.entities.HardwareConfigEntity; -import org.nrg.jobtemplates.models.ComputeSpecConfig; -import org.nrg.jobtemplates.repositories.ComputeSpecConfigDao; -import org.nrg.jobtemplates.repositories.HardwareConfigDao; -import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; +import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.models.ComputeSpecConfig; +import org.nrg.xnat.compute.repositories.ComputeSpecConfigDao; +import org.nrg.xnat.compute.repositories.HardwareConfigDao; +import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityService.java similarity index 60% rename from src/main/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityService.java rename to src/main/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityService.java index bfde7fd..76dc177 100644 --- a/src/main/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityService.java @@ -1,10 +1,10 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import lombok.extern.slf4j.Slf4j; import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; -import org.nrg.jobtemplates.entities.ConstraintConfigEntity; -import org.nrg.jobtemplates.repositories.ConstraintConfigDao; -import org.nrg.jobtemplates.services.ConstraintConfigEntityService; +import org.nrg.xnat.compute.entities.ConstraintConfigEntity; +import org.nrg.xnat.compute.repositories.ConstraintConfigDao; +import org.nrg.xnat.compute.services.ConstraintConfigEntityService; import org.springframework.stereotype.Service; @Service diff --git a/src/main/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityService.java similarity index 60% rename from src/main/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityService.java rename to src/main/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityService.java index 0b6db7a..c70a80d 100644 --- a/src/main/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityService.java @@ -1,10 +1,10 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import lombok.extern.slf4j.Slf4j; import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; -import org.nrg.jobtemplates.entities.HardwareConfigEntity; -import org.nrg.jobtemplates.repositories.HardwareConfigDao; -import org.nrg.jobtemplates.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.repositories.HardwareConfigDao; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; import org.springframework.stereotype.Service; @Service diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java index dbf2c84..62fe5ab 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java @@ -2,7 +2,7 @@ import lombok.extern.slf4j.Slf4j; import org.nrg.framework.annotations.XnatPlugin; -import org.nrg.jobtemplates.config.JobTemplatesConfig; +import org.nrg.xnat.compute.config.ComputeConfig; import org.nrg.xdat.security.helpers.UserHelper; import org.nrg.xdat.security.services.SearchHelperServiceI; import org.nrg.xnatx.plugins.jupyterhub.client.DefaultJupyterHubClient; @@ -17,10 +17,10 @@ import java.util.concurrent.TimeUnit; -@XnatPlugin(value = "JupyterHubPlugin", - name = "Jupyter Hub Plugin", +@XnatPlugin(value = "jupyterHubPlugin", + name = "XNAT JupyterHub Plugin", logConfigurationFile = "jupyterhub-logback.xml", - entityPackages = {"org.nrg.xnatx.plugins.jupyterhub.entities", "org.nrg.jobtemplates.entities"}) + entityPackages = {"org.nrg.xnatx.plugins.jupyterhub.entities", "org.nrg.xnat.compute.entities"}) @ComponentScan({"org.nrg.xnatx.plugins.jupyterhub.preferences", "org.nrg.xnatx.plugins.jupyterhub.client", "org.nrg.xnatx.plugins.jupyterhub.rest", @@ -32,7 +32,7 @@ "org.nrg.xnatx.plugins.jupyterhub.utils", "org.nrg.xnatx.plugins.jupyterhub.authorization", "org.nrg.xnatx.plugins.jupyterhub.initialize"}) -@Import({JobTemplatesConfig.class}) +@Import({ComputeConfig.class}) @Slf4j public class JupyterHubPlugin { diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java index 231db15..5cdd5c2 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java @@ -2,9 +2,9 @@ import lombok.extern.slf4j.Slf4j; import org.nrg.framework.constants.Scope; -import org.nrg.jobtemplates.models.*; -import org.nrg.jobtemplates.services.ComputeSpecConfigService; -import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xnat.compute.models.*; +import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; import org.nrg.xnat.initialization.tasks.InitializingTaskException; import org.nrg.xnat.services.XnatAppInfo; diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java index 8360ca8..4f5f371 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java @@ -3,7 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.nrg.framework.services.NrgEventServiceI; -import org.nrg.jobtemplates.services.JobTemplateService; +import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xdat.security.user.exceptions.UserInitException; import org.nrg.xft.security.UserI; diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java index 8bab1c8..ecdb529 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java @@ -1,10 +1,8 @@ package org.nrg.xnatx.plugins.jupyterhub.services.impl; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import lombok.NonNull; -import org.nrg.jobtemplates.models.*; -import org.nrg.jobtemplates.services.JobTemplateService; import org.nrg.xdat.entities.AliasToken; import org.nrg.xdat.model.XnatSubjectassessordataI; import org.nrg.xdat.om.*; @@ -17,6 +15,8 @@ import org.nrg.xft.exception.XFTInitException; import org.nrg.xft.schema.Wrappers.GenericWrapper.GenericWrapperElement; import org.nrg.xft.security.UserI; +import org.nrg.xnat.compute.models.*; +import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xnat.exceptions.InvalidArchiveStructure; import org.nrg.xnatx.plugins.jupyterhub.entities.UserOptionsEntity; import org.nrg.xnatx.plugins.jupyterhub.models.XnatUserOptions; diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/compute-spec-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-spec-configs.js similarity index 94% rename from src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/compute-spec-configs.js rename to src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-spec-configs.js index 51f3afc..10caadb 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/compute-spec-configs.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-spec-configs.js @@ -1,10 +1,8 @@ console.debug("Loading compute-spec-configs.js"); var XNAT = getObject(XNAT || {}); -XNAT.app = getObject(XNAT.app || {}); -XNAT.plugin = getObject(XNAT.plugin || {}); -XNAT.plugin.jobTemplates = getObject(XNAT.plugin.jobTemplates|| {}); -XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates.computeSpecConfigs || {}); +XNAT.compute = getObject(XNAT.compute|| {}); +XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || {}); (function (factory) { if (typeof define === 'function' && define.amd) { @@ -16,9 +14,9 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates } }(function () { - XNAT.plugin.jobTemplates.computeSpecConfigs.get = async (id) => { + XNAT.compute.computeSpecConfigs.get = async (id) => { console.debug("Fetching compute spec config " + id) - const url = XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs/${id}`); + const url = XNAT.url.csrfUrl(`/xapi/compute-spec-configs/${id}`); const response = await fetch(url, { method: 'GET', @@ -34,12 +32,12 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates } } - XNAT.plugin.jobTemplates.computeSpecConfigs.getAll = async (type) => { + XNAT.compute.computeSpecConfigs.getAll = async (type) => { console.debug("Fetching compute spec configs") const url = type ? - XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs?type=${type}`) : - XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs`); + XNAT.url.csrfUrl(`/xapi/compute-spec-configs?type=${type}`) : + XNAT.url.csrfUrl(`/xapi/compute-spec-configs`); const response = await fetch(url, { method: 'GET', @@ -55,9 +53,9 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates } } - XNAT.plugin.jobTemplates.computeSpecConfigs.create = async (computeSpec) => { + XNAT.compute.computeSpecConfigs.create = async (computeSpec) => { console.debug("Creating compute spec config") - const url = XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs`); + const url = XNAT.url.csrfUrl(`/xapi/compute-spec-configs`); const response = await fetch(url, { method: 'POST', @@ -74,7 +72,7 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates } } - XNAT.plugin.jobTemplates.computeSpecConfigs.update = async (computeSpec) => { + XNAT.compute.computeSpecConfigs.update = async (computeSpec) => { console.debug("Updating compute spec config") const id = computeSpec['id']; @@ -82,7 +80,7 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates throw new Error(`Cannot update compute spec config without an ID`); } - const url = XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs/${id}`); + const url = XNAT.url.csrfUrl(`/xapi/compute-spec-configs/${id}`); const response = await fetch(url, { method: 'PUT', @@ -99,20 +97,20 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates } } - XNAT.plugin.jobTemplates.computeSpecConfigs.save = async (computeSpec) => { + XNAT.compute.computeSpecConfigs.save = async (computeSpec) => { console.debug("Saving compute spec config") const id = computeSpec['id']; if (id) { - return XNAT.plugin.jobTemplates.computeSpecConfigs.update(computeSpec); + return XNAT.compute.computeSpecConfigs.update(computeSpec); } else { - return XNAT.plugin.jobTemplates.computeSpecConfigs.create(computeSpec); + return XNAT.compute.computeSpecConfigs.create(computeSpec); } } - XNAT.plugin.jobTemplates.computeSpecConfigs.delete = async (id) => { + XNAT.compute.computeSpecConfigs.delete = async (id) => { console.debug("Deleting compute spec config " + id) - const url = XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs/${id}`); + const url = XNAT.url.csrfUrl(`/xapi/compute-spec-configs/${id}`); const response = await fetch(url, { method: 'DELETE', @@ -123,11 +121,11 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates } } - XNAT.plugin.jobTemplates.computeSpecConfigs.available = async (type, user, project) => { + XNAT.compute.computeSpecConfigs.available = async (type, user, project) => { console.debug(`Fetching available compute spec configs for type ${type} and user ${user} and project ${project}`); const url = type ? - XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs/available?type=${type}&user=${user}&project=${project}`) : - XNAT.url.csrfUrl(`/xapi/job-templates/compute-spec-configs/available?user=${user}&project=${project}`); + XNAT.url.csrfUrl(`/xapi/compute-spec-configs/available?type=${type}&user=${user}&project=${project}`) : + XNAT.url.csrfUrl(`/xapi/compute-spec-configs/available?user=${user}&project=${project}`); const response = await fetch(url, { method: 'GET', @@ -143,7 +141,7 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates } } - XNAT.plugin.jobTemplates.computeSpecConfigs.manager = async (containerId, computeSpecType) => { + XNAT.compute.computeSpecConfigs.manager = async (containerId, computeSpecType) => { console.debug("Initializing compute spec manager") let container, @@ -195,7 +193,7 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates } const getHardwareConfigs = () => { - XNAT.plugin.jobTemplates.hardwareConfigs.getAll().then(h => hardwareConfigs = h); + XNAT.compute.hardwareConfigs.getAll().then(h => hardwareConfigs = h); } const clearContainer = () => { @@ -227,7 +225,7 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates let enabled = ckbox.checked; config['scopes']['Site']['enabled'] = enabled; - XNAT.plugin.jobTemplates.computeSpecConfigs.update(config).then(() => { + XNAT.compute.computeSpecConfigs.update(config).then(() => { XNAT.ui.banner.top(2000, `Compute Spec ${enabled ? 'Enabled' : 'Disabled'}`, 'success'); }).catch((err) => { console.error(err); @@ -726,11 +724,11 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates } } - XNAT.plugin.jobTemplates.computeSpecConfigs.save(config) + XNAT.compute.computeSpecConfigs.save(config) .then(() => { refreshTable(); XNAT.dialog.closeAll(); - XNAT.ui.banner.top(2000, 'Save', 'success'); + XNAT.ui.banner.top(2000, 'Saved', 'success'); }) .catch((err) => { console.error(err); @@ -743,7 +741,7 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates } const refreshTable = () => { - XNAT.plugin.jobTemplates.computeSpecConfigs.getAll(computeSpecType) + XNAT.compute.computeSpecConfigs.getAll(computeSpecType) .then((data) => { computeSpecConfigs = data; @@ -763,7 +761,7 @@ XNAT.plugin.jobTemplates.computeSpecConfigs = getObject(XNAT.plugin.jobTemplates } const deleteConfig = (id) => { - XNAT.plugin.jobTemplates.computeSpecConfigs.delete(id) + XNAT.compute.computeSpecConfigs.delete(id) .then(() => { XNAT.ui.banner.top(2000, 'Delete successful', 'success'); refreshTable(); diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/constraint-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/constraint-configs.js similarity index 93% rename from src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/constraint-configs.js rename to src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/constraint-configs.js index a39f691..13f19ad 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/constraint-configs.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/constraint-configs.js @@ -1,10 +1,8 @@ console.debug('Loading constraint-configs.js') var XNAT = getObject(XNAT || {}); -XNAT.app = getObject(XNAT.app || {}); -XNAT.plugin = getObject(XNAT.plugin || {}); -XNAT.plugin.jobTemplates = getObject(XNAT.plugin.jobTemplates|| {}); -XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates.constraintConfigs || {}); +XNAT.compute = getObject(XNAT.compute|| {}); +XNAT.compute.constraintConfigs = getObject(XNAT.compute.constraintConfigs || {}); (function (factory) { if (typeof define === 'function' && define.amd) { @@ -16,9 +14,9 @@ XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates. } }(function () { - XNAT.plugin.jobTemplates.constraintConfigs.get = async (id) => { + XNAT.compute.constraintConfigs.get = async (id) => { console.debug(`Fetching constraint config ${id}`); - const url = XNAT.url.restUrl(`/xapi/job-templates/constraint-configs/${id}`); + const url = XNAT.url.restUrl(`/xapi/constraint-configs/${id}`); const response = await fetch(url, { method: 'GET', @@ -34,9 +32,9 @@ XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates. } } - XNAT.plugin.jobTemplates.constraintConfigs.getAll = async () => { + XNAT.compute.constraintConfigs.getAll = async () => { console.debug('Fetching all constraint configs'); - const url = XNAT.url.restUrl('/xapi/job-templates/constraint-configs'); + const url = XNAT.url.restUrl('/xapi/constraint-configs'); const response = await fetch(url, { method: 'GET', @@ -52,9 +50,9 @@ XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates. } } - XNAT.plugin.jobTemplates.constraintConfigs.create = async (constraintConfig) => { + XNAT.compute.constraintConfigs.create = async (constraintConfig) => { console.debug('Creating constraint config'); - const url = XNAT.url.restUrl('/xapi/job-templates/constraint-configs'); + const url = XNAT.url.restUrl('/xapi/constraint-configs'); const response = await fetch(url, { method: 'POST', @@ -71,9 +69,9 @@ XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates. } } - XNAT.plugin.jobTemplates.constraintConfigs.update = async (constraintConfig) => { + XNAT.compute.constraintConfigs.update = async (constraintConfig) => { console.debug(`Updating constraint config ${constraintConfig.id}`); - const url = XNAT.url.restUrl(`/xapi/job-templates/constraint-configs/${constraintConfig.id}`); + const url = XNAT.url.restUrl(`/xapi/constraint-configs/${constraintConfig.id}`); const response = await fetch(url, { method: 'PUT', @@ -90,9 +88,9 @@ XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates. } } - XNAT.plugin.jobTemplates.constraintConfigs.delete = async (id) => { + XNAT.compute.constraintConfigs.delete = async (id) => { console.debug(`Deleting constraint config ${id}`); - const url = XNAT.url.restUrl(`/xapi/job-templates/constraint-configs/${id}`); + const url = XNAT.url.restUrl(`/xapi/constraint-configs/${id}`); const response = await fetch(url, { method: 'DELETE', @@ -103,16 +101,16 @@ XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates. } } - XNAT.plugin.jobTemplates.constraintConfigs.save = async (constraintConfig) => { + XNAT.compute.constraintConfigs.save = async (constraintConfig) => { console.debug(`Saving constraint config ${constraintConfig.id}`); if (constraintConfig.id) { - return XNAT.plugin.jobTemplates.constraintConfigs.update(constraintConfig); + return XNAT.compute.constraintConfigs.update(constraintConfig); } else { - return XNAT.plugin.jobTemplates.constraintConfigs.create(constraintConfig); + return XNAT.compute.constraintConfigs.create(constraintConfig); } } - XNAT.plugin.jobTemplates.constraintConfigs.manager = async (containerId) => { + XNAT.compute.constraintConfigs.manager = async (containerId) => { console.debug(`Initializing constraint config manager in container ${containerId}`); let container, @@ -166,7 +164,7 @@ XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates. } const deleteConfig = (id) => { - XNAT.plugin.jobTemplates.constraintConfigs.delete(id).then(() => { + XNAT.compute.constraintConfigs.delete(id).then(() => { XNAT.ui.banner.top(2000, 'Constraint deleted', 'success'); refreshTable(); }).catch(err => { @@ -222,7 +220,7 @@ XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates. let enabled = ckbox.checked; config['scopes']['Site']['enabled'] = enabled; - XNAT.plugin.jobTemplates.constraintConfigs.update(config).then(() => { + XNAT.compute.constraintConfigs.update(config).then(() => { XNAT.ui.banner.top(2000, `Constraint ${enabled ? 'Enabled' : 'Disabled'}`, 'success'); }).catch((err) => { console.error(err); @@ -495,7 +493,7 @@ XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates. } } - XNAT.plugin.jobTemplates.constraintConfigs.save(config).then(() => { + XNAT.compute.constraintConfigs.save(config).then(() => { XNAT.ui.banner.top(2000, 'Saved constraint config', 'success'); XNAT.dialog.closeAll(); refreshTable(); @@ -515,7 +513,7 @@ XNAT.plugin.jobTemplates.constraintConfigs = getObject(XNAT.plugin.jobTemplates. } const refreshTable = () => { - XNAT.plugin.jobTemplates.constraintConfigs.getAll().then(data => { + XNAT.compute.constraintConfigs.getAll().then(data => { constraintConfigs = data; if (constraintConfigs.length === 0) { diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/hardware-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/hardware-configs.js similarity index 96% rename from src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/hardware-configs.js rename to src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/hardware-configs.js index 9528f42..7ecdef0 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jobTemplates/hardware-configs.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/hardware-configs.js @@ -1,10 +1,8 @@ console.debug("Loading hardware-configs.js"); var XNAT = getObject(XNAT || {}); -XNAT.app = getObject(XNAT.app || {}); -XNAT.plugin = getObject(XNAT.plugin || {}); -XNAT.plugin.jobTemplates = getObject(XNAT.plugin.jobTemplates || {}); -XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.hardwareConfigs || {}); +XNAT.compute = getObject(XNAT.compute || {}); +XNAT.compute.hardwareConfigs = getObject(XNAT.compute.hardwareConfigs || {}); (function (factory) { if (typeof define === 'function' && define.amd) { @@ -16,9 +14,9 @@ XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.ha } }(function () { - XNAT.plugin.jobTemplates.hardwareConfigs.get = async (id) => { + XNAT.compute.hardwareConfigs.get = async (id) => { console.debug("Fetching hardware config with id: " + id); - const url = XNAT.url.restUrl('/xapi/job-templates/hardware-configs/' + id); + const url = XNAT.url.restUrl('/xapi/hardware-configs/' + id); const response = await fetch(url, { method: 'GET', @@ -34,9 +32,9 @@ XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.ha } } - XNAT.plugin.jobTemplates.hardwareConfigs.getAll = async () => { + XNAT.compute.hardwareConfigs.getAll = async () => { console.debug("Fetching all hardware configs"); - const url = XNAT.url.restUrl('/xapi/job-templates/hardware-configs'); + const url = XNAT.url.restUrl('/xapi/hardware-configs'); const response = await fetch(url, { method: 'GET', @@ -52,9 +50,9 @@ XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.ha } } - XNAT.plugin.jobTemplates.hardwareConfigs.create = async (config) => { + XNAT.compute.hardwareConfigs.create = async (config) => { console.debug("Creating hardware config"); - const url = XNAT.url.restUrl('/xapi/job-templates/hardware-configs'); + const url = XNAT.url.restUrl('/xapi/hardware-configs'); const response = await fetch(url, { method: 'POST', @@ -71,7 +69,7 @@ XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.ha } }; - XNAT.plugin.jobTemplates.hardwareConfigs.update = async (config) => { + XNAT.compute.hardwareConfigs.update = async (config) => { console.debug("Updating hardware config"); const id = config.id; @@ -79,7 +77,7 @@ XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.ha throw new Error('Hardware config id is required'); } - const url = XNAT.url.restUrl(`/xapi/job-templates/hardware-configs/${id}`); + const url = XNAT.url.restUrl(`/xapi/hardware-configs/${id}`); const response = await fetch(url, { method: 'PUT', @@ -96,20 +94,20 @@ XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.ha } } - XNAT.plugin.jobTemplates.hardwareConfigs.save = async (config) => { + XNAT.compute.hardwareConfigs.save = async (config) => { console.debug("Saving hardware config"); const id = config.id; if (!id) { - return XNAT.plugin.jobTemplates.hardwareConfigs.create(config); + return XNAT.compute.hardwareConfigs.create(config); } else { - return XNAT.plugin.jobTemplates.hardwareConfigs.update(config); + return XNAT.compute.hardwareConfigs.update(config); } } - XNAT.plugin.jobTemplates.hardwareConfigs.delete = async (id) => { + XNAT.compute.hardwareConfigs.delete = async (id) => { console.debug("Deleting hardware config with id: " + id); - const url = XNAT.url.restUrl('/xapi/job-templates/hardware-configs/' + id); + const url = XNAT.url.restUrl('/xapi/hardware-configs/' + id); const response = await fetch(url, { method: 'DELETE', @@ -123,7 +121,7 @@ XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.ha } } - XNAT.plugin.jobTemplates.hardwareConfigs.manager = async (containerId) => { + XNAT.compute.hardwareConfigs.manager = async (containerId) => { console.debug("Initializing hardware config manager"); let container, @@ -201,7 +199,7 @@ XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.ha let enabled = ckbox.checked; config['scopes']['Site']['enabled'] = enabled; - XNAT.plugin.jobTemplates.hardwareConfigs.update(config).then(() => { + XNAT.compute.hardwareConfigs.update(config).then(() => { XNAT.ui.banner.top(2000, `Hardware ${enabled ? 'Enabled' : 'Disabled'}`, 'success'); }).catch((err) => { console.error(err); @@ -705,7 +703,7 @@ XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.ha } } - XNAT.plugin.jobTemplates.hardwareConfigs.save(config).then(() => { + XNAT.compute.hardwareConfigs.save(config).then(() => { XNAT.ui.banner.top(2000, 'Hardware saved', 'success'); XNAT.dialog.closeAll(); refreshTable(); @@ -726,7 +724,7 @@ XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.ha } const refreshTable = () => { - XNAT.plugin.jobTemplates.hardwareConfigs.getAll().then(data => { + XNAT.compute.hardwareConfigs.getAll().then(data => { hardwareConfigs = data; if (hardwareConfigs.length === 0) { @@ -756,7 +754,7 @@ XNAT.plugin.jobTemplates.hardwareConfigs = getObject(XNAT.plugin.jobTemplates.ha } const deleteConfig = (id) => { - XNAT.plugin.jobTemplates.hardwareConfigs.delete(id).then(() => { + XNAT.compute.hardwareConfigs.delete(id).then(() => { XNAT.ui.banner.top(2000, 'Hardware config deleted', 'success'); refreshTable(); }).catch(err => { diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js index f33feb8..47eb524 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js @@ -176,7 +176,7 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s console.debug(`jupyterhub-servers.js: XNAT.plugin.jupyterhub.servers.startServer`); console.debug(`Launching jupyter server. User: ${username}, Server Name: ${servername}, XSI Type: ${xsiType}, ID: ${itemId}, Label: ${itemLabel}, Project ID: ${projectId}, eventTrackingId: ${eventTrackingId}`); - XNAT.plugin.jobTemplates.computeSpecConfigs.available("JUPYTERHUB", username, projectId).then(computeSpecConfigs => { + XNAT.compute.computeSpecConfigs.available("JUPYTERHUB", username, projectId).then(computeSpecConfigs => { const cancelButton = { label: 'Cancel', isDefault: false, diff --git a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm index 41e5795..d8d4076 100644 --- a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm +++ b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm @@ -12,5 +12,5 @@ - - \ No newline at end of file + + \ No newline at end of file diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index 46840c2..ff06aaf 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -179,13 +179,13 @@ jupyterEnvironments: computeSpecsTable: tag: "div#jupyterhub-compute-spec-configs-table" computeSpecsScript: - tag: script|src="~/scripts/xnat/plugin/jobTemplates/compute-spec-configs.js" + tag: script|src="~/scripts/xnat/plugin/compute/compute-spec-configs.js" hardwareConfigsScript: - tag: script|src="~/scripts/xnat/plugin/jobTemplates/hardware-configs.js" + tag: script|src="~/scripts/xnat/plugin/compute/hardware-configs.js" renderComputeSpecsTable: tag: script content: > - XNAT.plugin.jobTemplates.computeSpecConfigs.manager('jupyterhub-compute-spec-configs-table', 'JUPYTERHUB'); + XNAT.compute.computeSpecConfigs.manager('jupyterhub-compute-spec-configs-table', 'JUPYTERHUB'); hardwareConfigs: kind: panel @@ -203,11 +203,11 @@ hardwareConfigs: hardwareConfigsTable: tag: "div#jupyterhub-hardware-configs-table" hardwareConfigsScript: - tag: script|src="~/scripts/xnat/plugin/jobTemplates/hardware-configs.js" + tag: script|src="~/scripts/xnat/plugin/compute/hardware-configs.js" renderHardwareConfigsTable: tag: script content: > - XNAT.plugin.jobTemplates.hardwareConfigs.manager('jupyterhub-hardware-configs-table'); + XNAT.compute.hardwareConfigs.manager('jupyterhub-hardware-configs-table'); constraints: kind: panel @@ -225,12 +225,12 @@ constraints: constraintsTable: tag: "div#jupyterhub-constraints-table" constraintsScript: - tag: script|src="~/scripts/xnat/plugin/jobTemplates/constraint-configs.js" + tag: script|src="~/scripts/xnat/plugin/compute/constraint-configs.js" renderConstraintsTable: tag: script content: > console.log('rendering constraints table'); - XNAT.plugin.jobTemplates.constraintConfigs.manager('jupyterhub-constraints-table'); + XNAT.compute.constraintConfigs.manager('jupyterhub-constraints-table'); jupyterhubPreferences: kind: panel.form diff --git a/src/main/resources/jupyterhub-logback.xml b/src/main/resources/jupyterhub-logback.xml index ec35cc2..5eb4494 100644 --- a/src/main/resources/jupyterhub-logback.xml +++ b/src/main/resources/jupyterhub-logback.xml @@ -10,20 +10,20 @@ ${xnat.home}/logs/xnat-jupyterhub-plugin.log.%d{yyyy-MM-dd} - + true - ${xnat.home}/logs/job-templates.log + ${xnat.home}/logs/compute.log %d [%t] %-5p %c - %m%n - ${xnat.home}/logs/job-templates.log.%d{yyyy-MM-dd} + ${xnat.home}/logs/compute.log.%d{yyyy-MM-dd} - - + + diff --git a/src/test/java/org/nrg/jobtemplates/config/ComputeSpecConfigsApiConfig.java b/src/test/java/org/nrg/xnat/compute/config/ComputeSpecConfigsApiConfig.java similarity index 92% rename from src/test/java/org/nrg/jobtemplates/config/ComputeSpecConfigsApiConfig.java rename to src/test/java/org/nrg/xnat/compute/config/ComputeSpecConfigsApiConfig.java index af84967..078989b 100644 --- a/src/test/java/org/nrg/jobtemplates/config/ComputeSpecConfigsApiConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/ComputeSpecConfigsApiConfig.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; import org.nrg.framework.services.ContextService; -import org.nrg.jobtemplates.rest.ComputeSpecConfigsApi; -import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.rest.ComputeSpecConfigsApi; +import org.nrg.xnat.compute.services.ComputeSpecConfigService; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; import org.springframework.context.ApplicationContext; diff --git a/src/test/java/org/nrg/jobtemplates/config/ConstraintConfigsApiTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/ConstraintConfigsApiTestConfig.java similarity index 92% rename from src/test/java/org/nrg/jobtemplates/config/ConstraintConfigsApiTestConfig.java rename to src/test/java/org/nrg/xnat/compute/config/ConstraintConfigsApiTestConfig.java index 5adaee9..5e7fb16 100644 --- a/src/test/java/org/nrg/jobtemplates/config/ConstraintConfigsApiTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/ConstraintConfigsApiTestConfig.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; import org.nrg.framework.services.ContextService; -import org.nrg.jobtemplates.rest.ConstraintConfigsApi; -import org.nrg.jobtemplates.services.ConstraintConfigService; +import org.nrg.xnat.compute.rest.ConstraintConfigsApi; +import org.nrg.xnat.compute.services.ConstraintConfigService; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; import org.springframework.context.ApplicationContext; diff --git a/src/test/java/org/nrg/jobtemplates/config/DefaultComputeSpecConfigServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultComputeSpecConfigServiceTestConfig.java similarity index 76% rename from src/test/java/org/nrg/jobtemplates/config/DefaultComputeSpecConfigServiceTestConfig.java rename to src/test/java/org/nrg/xnat/compute/config/DefaultComputeSpecConfigServiceTestConfig.java index a2d339b..b6d197c 100644 --- a/src/test/java/org/nrg/jobtemplates/config/DefaultComputeSpecConfigServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/DefaultComputeSpecConfigServiceTestConfig.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; -import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; -import org.nrg.jobtemplates.services.HardwareConfigEntityService; -import org.nrg.jobtemplates.services.impl.DefaultComputeSpecConfigService; +import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.services.impl.DefaultComputeSpecConfigService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/src/test/java/org/nrg/jobtemplates/config/DefaultConstraintConfigServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultConstraintConfigServiceTestConfig.java similarity index 77% rename from src/test/java/org/nrg/jobtemplates/config/DefaultConstraintConfigServiceTestConfig.java rename to src/test/java/org/nrg/xnat/compute/config/DefaultConstraintConfigServiceTestConfig.java index f28f15c..d984e94 100644 --- a/src/test/java/org/nrg/jobtemplates/config/DefaultConstraintConfigServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/DefaultConstraintConfigServiceTestConfig.java @@ -1,7 +1,7 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; -import org.nrg.jobtemplates.services.ConstraintConfigEntityService; -import org.nrg.jobtemplates.services.impl.DefaultConstraintConfigService; +import org.nrg.xnat.compute.services.ConstraintConfigEntityService; +import org.nrg.xnat.compute.services.impl.DefaultConstraintConfigService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/src/test/java/org/nrg/jobtemplates/config/DefaultHardwareConfigServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultHardwareConfigServiceTestConfig.java similarity index 76% rename from src/test/java/org/nrg/jobtemplates/config/DefaultHardwareConfigServiceTestConfig.java rename to src/test/java/org/nrg/xnat/compute/config/DefaultHardwareConfigServiceTestConfig.java index 3c56586..a98126f 100644 --- a/src/test/java/org/nrg/jobtemplates/config/DefaultHardwareConfigServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/DefaultHardwareConfigServiceTestConfig.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; -import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; -import org.nrg.jobtemplates.services.HardwareConfigEntityService; -import org.nrg.jobtemplates.services.impl.DefaultHardwareConfigService; +import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.services.impl.DefaultHardwareConfigService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/src/test/java/org/nrg/jobtemplates/config/DefaultJobTemplateServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultJobTemplateServiceTestConfig.java similarity index 74% rename from src/test/java/org/nrg/jobtemplates/config/DefaultJobTemplateServiceTestConfig.java rename to src/test/java/org/nrg/xnat/compute/config/DefaultJobTemplateServiceTestConfig.java index 5ac44c0..52fd166 100644 --- a/src/test/java/org/nrg/jobtemplates/config/DefaultJobTemplateServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/DefaultJobTemplateServiceTestConfig.java @@ -1,9 +1,9 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; -import org.nrg.jobtemplates.services.ComputeSpecConfigService; -import org.nrg.jobtemplates.services.ConstraintConfigService; -import org.nrg.jobtemplates.services.HardwareConfigService; -import org.nrg.jobtemplates.services.impl.DefaultJobTemplateService; +import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.ConstraintConfigService; +import org.nrg.xnat.compute.services.HardwareConfigService; +import org.nrg.xnat.compute.services.impl.DefaultJobTemplateService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/src/test/java/org/nrg/jobtemplates/config/HardwareConfigsApiConfig.java b/src/test/java/org/nrg/xnat/compute/config/HardwareConfigsApiConfig.java similarity index 92% rename from src/test/java/org/nrg/jobtemplates/config/HardwareConfigsApiConfig.java rename to src/test/java/org/nrg/xnat/compute/config/HardwareConfigsApiConfig.java index 267faed..0571b52 100644 --- a/src/test/java/org/nrg/jobtemplates/config/HardwareConfigsApiConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/HardwareConfigsApiConfig.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; import org.nrg.framework.services.ContextService; -import org.nrg.jobtemplates.rest.HardwareConfigsApi; -import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xnat.compute.rest.HardwareConfigsApi; +import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; import org.springframework.context.ApplicationContext; diff --git a/src/test/java/org/nrg/jobtemplates/config/HibernateComputeSpecConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateComputeSpecConfigEntityServiceTestConfig.java similarity index 90% rename from src/test/java/org/nrg/jobtemplates/config/HibernateComputeSpecConfigEntityServiceTestConfig.java rename to src/test/java/org/nrg/xnat/compute/config/HibernateComputeSpecConfigEntityServiceTestConfig.java index 569a9ac..9f26146 100644 --- a/src/test/java/org/nrg/jobtemplates/config/HibernateComputeSpecConfigEntityServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/HibernateComputeSpecConfigEntityServiceTestConfig.java @@ -1,11 +1,11 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; import org.hibernate.SessionFactory; -import org.nrg.jobtemplates.entities.*; -import org.nrg.jobtemplates.repositories.ComputeSpecConfigDao; -import org.nrg.jobtemplates.repositories.ConstraintConfigDao; -import org.nrg.jobtemplates.repositories.HardwareConfigDao; -import org.nrg.jobtemplates.services.impl.HibernateComputeSpecConfigEntityService; +import org.nrg.xnat.compute.entities.*; +import org.nrg.xnat.compute.repositories.ComputeSpecConfigDao; +import org.nrg.xnat.compute.repositories.ConstraintConfigDao; +import org.nrg.xnat.compute.repositories.HardwareConfigDao; +import org.nrg.xnat.compute.services.impl.HibernateComputeSpecConfigEntityService; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/test/java/org/nrg/jobtemplates/config/HibernateConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateConfig.java similarity index 97% rename from src/test/java/org/nrg/jobtemplates/config/HibernateConfig.java rename to src/test/java/org/nrg/xnat/compute/config/HibernateConfig.java index aaaa816..699040f 100644 --- a/src/test/java/org/nrg/jobtemplates/config/HibernateConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/HibernateConfig.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; import org.apache.commons.dbcp2.BasicDataSource; import org.hibernate.SessionFactory; -import org.nrg.jobtemplates.entities.*; +import org.nrg.xnat.compute.entities.*; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.context.annotation.Bean; diff --git a/src/test/java/org/nrg/jobtemplates/config/HibernateConstraintConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateConstraintConfigEntityServiceTestConfig.java similarity index 91% rename from src/test/java/org/nrg/jobtemplates/config/HibernateConstraintConfigEntityServiceTestConfig.java rename to src/test/java/org/nrg/xnat/compute/config/HibernateConstraintConfigEntityServiceTestConfig.java index 4c11307..c1bc22b 100644 --- a/src/test/java/org/nrg/jobtemplates/config/HibernateConstraintConfigEntityServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/HibernateConstraintConfigEntityServiceTestConfig.java @@ -1,9 +1,9 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; import org.hibernate.SessionFactory; -import org.nrg.jobtemplates.entities.*; -import org.nrg.jobtemplates.repositories.ConstraintConfigDao; -import org.nrg.jobtemplates.services.impl.HibernateConstraintConfigEntityService; +import org.nrg.xnat.compute.entities.*; +import org.nrg.xnat.compute.repositories.ConstraintConfigDao; +import org.nrg.xnat.compute.services.impl.HibernateConstraintConfigEntityService; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/test/java/org/nrg/jobtemplates/config/HibernateEntityServicesConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateEntityServicesConfig.java similarity index 76% rename from src/test/java/org/nrg/jobtemplates/config/HibernateEntityServicesConfig.java rename to src/test/java/org/nrg/xnat/compute/config/HibernateEntityServicesConfig.java index f251ec7..82aa41d 100644 --- a/src/test/java/org/nrg/jobtemplates/config/HibernateEntityServicesConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/HibernateEntityServicesConfig.java @@ -1,15 +1,15 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; import org.hibernate.SessionFactory; -import org.nrg.jobtemplates.repositories.ComputeSpecConfigDao; -import org.nrg.jobtemplates.repositories.ConstraintConfigDao; -import org.nrg.jobtemplates.repositories.HardwareConfigDao; -import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; -import org.nrg.jobtemplates.services.ConstraintConfigEntityService; -import org.nrg.jobtemplates.services.HardwareConfigEntityService; -import org.nrg.jobtemplates.services.impl.HibernateComputeSpecConfigEntityService; -import org.nrg.jobtemplates.services.impl.HibernateConstraintConfigEntityService; -import org.nrg.jobtemplates.services.impl.HibernateHardwareConfigEntityService; +import org.nrg.xnat.compute.repositories.ComputeSpecConfigDao; +import org.nrg.xnat.compute.repositories.ConstraintConfigDao; +import org.nrg.xnat.compute.repositories.HardwareConfigDao; +import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.ConstraintConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.services.impl.HibernateComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.impl.HibernateConstraintConfigEntityService; +import org.nrg.xnat.compute.services.impl.HibernateHardwareConfigEntityService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/src/test/java/org/nrg/jobtemplates/config/HibernateHardwareConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateHardwareConfigEntityServiceTestConfig.java similarity index 91% rename from src/test/java/org/nrg/jobtemplates/config/HibernateHardwareConfigEntityServiceTestConfig.java rename to src/test/java/org/nrg/xnat/compute/config/HibernateHardwareConfigEntityServiceTestConfig.java index 67b2541..49afb29 100644 --- a/src/test/java/org/nrg/jobtemplates/config/HibernateHardwareConfigEntityServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/HibernateHardwareConfigEntityServiceTestConfig.java @@ -1,9 +1,9 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; import org.hibernate.SessionFactory; -import org.nrg.jobtemplates.entities.*; -import org.nrg.jobtemplates.repositories.ConstraintConfigDao; -import org.nrg.jobtemplates.services.impl.HibernateHardwareConfigEntityService; +import org.nrg.xnat.compute.entities.*; +import org.nrg.xnat.compute.repositories.ConstraintConfigDao; +import org.nrg.xnat.compute.services.impl.HibernateHardwareConfigEntityService; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/test/java/org/nrg/jobtemplates/config/MockConfig.java b/src/test/java/org/nrg/xnat/compute/config/MockConfig.java similarity index 94% rename from src/test/java/org/nrg/jobtemplates/config/MockConfig.java rename to src/test/java/org/nrg/xnat/compute/config/MockConfig.java index 2ae4ea0..6117afc 100644 --- a/src/test/java/org/nrg/jobtemplates/config/MockConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/MockConfig.java @@ -1,13 +1,13 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; import org.mockito.Mockito; import org.nrg.framework.services.SerializerService; -import org.nrg.jobtemplates.repositories.ComputeSpecConfigDao; -import org.nrg.jobtemplates.repositories.HardwareConfigDao; -import org.nrg.jobtemplates.services.*; +import org.nrg.xnat.compute.repositories.ComputeSpecConfigDao; +import org.nrg.xnat.compute.repositories.HardwareConfigDao; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.RoleServiceI; import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnat.compute.services.*; import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; import org.springframework.beans.factory.annotation.Qualifier; diff --git a/src/test/java/org/nrg/jobtemplates/config/ObjectMapperConfig.java b/src/test/java/org/nrg/xnat/compute/config/ObjectMapperConfig.java similarity index 97% rename from src/test/java/org/nrg/jobtemplates/config/ObjectMapperConfig.java rename to src/test/java/org/nrg/xnat/compute/config/ObjectMapperConfig.java index c3776b5..eb5b5ad 100644 --- a/src/test/java/org/nrg/jobtemplates/config/ObjectMapperConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/ObjectMapperConfig.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/test/java/org/nrg/jobtemplates/config/RestApiTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/RestApiTestConfig.java similarity index 98% rename from src/test/java/org/nrg/jobtemplates/config/RestApiTestConfig.java rename to src/test/java/org/nrg/xnat/compute/config/RestApiTestConfig.java index 15437fd..6016ff6 100644 --- a/src/test/java/org/nrg/jobtemplates/config/RestApiTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/RestApiTestConfig.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.config; +package org.nrg.xnat.compute.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.mockito.Mockito; diff --git a/src/test/java/org/nrg/jobtemplates/models/ConstraintTest.java b/src/test/java/org/nrg/xnat/compute/models/ConstraintTest.java similarity index 92% rename from src/test/java/org/nrg/jobtemplates/models/ConstraintTest.java rename to src/test/java/org/nrg/xnat/compute/models/ConstraintTest.java index 10beb91..cfcb81d 100644 --- a/src/test/java/org/nrg/jobtemplates/models/ConstraintTest.java +++ b/src/test/java/org/nrg/xnat/compute/models/ConstraintTest.java @@ -1,6 +1,7 @@ -package org.nrg.jobtemplates.models; +package org.nrg.xnat.compute.models; import org.junit.Test; +import org.nrg.xnat.compute.models.Constraint; import java.util.Arrays; import java.util.HashSet; diff --git a/src/test/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApiTest.java b/src/test/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApiTest.java similarity index 91% rename from src/test/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApiTest.java rename to src/test/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApiTest.java index 1afd407..56d25fb 100644 --- a/src/test/java/org/nrg/jobtemplates/rest/ComputeSpecConfigsApiTest.java +++ b/src/test/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApiTest.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.rest; +package org.nrg.xnat.compute.rest; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.After; @@ -6,9 +6,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.nrg.jobtemplates.config.ComputeSpecConfigsApiConfig; -import org.nrg.jobtemplates.models.ComputeSpecConfig; -import org.nrg.jobtemplates.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.config.ComputeSpecConfigsApiConfig; +import org.nrg.xnat.compute.models.ComputeSpecConfig; +import org.nrg.xnat.compute.services.ComputeSpecConfigService; import org.nrg.xdat.security.services.RoleServiceI; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xft.security.UserI; @@ -81,7 +81,7 @@ public void after() throws Exception { @Test public void testGetAllComputeSpecConfigs() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/job-templates/compute-spec-configs") + .get("/compute-spec-configs") .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); @@ -96,7 +96,7 @@ public void testGetAllComputeSpecConfigs() throws Exception { @Test public void testGetComputeSpecConfigsByType() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/job-templates/compute-spec-configs") + .get("/compute-spec-configs") .param("type", "JUPYTERHUB") .with(authentication(mockAuthentication)) .with(csrf()) @@ -112,7 +112,7 @@ public void testGetComputeSpecConfigsByType() throws Exception { @Test public void testGetComputeSpecConfigById() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/job-templates/compute-spec-configs/1") + .get("/compute-spec-configs/1") .accept(APPLICATION_JSON) .with(authentication(mockAuthentication)) .with(csrf()) @@ -129,7 +129,7 @@ public void testGetComputeSpecConfigById() throws Exception { @Test public void testGetComputeSpecConfigByIdNotFound() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/job-templates/compute-spec-configs/1") + .get("/compute-spec-configs/1") .accept(APPLICATION_JSON) .with(authentication(mockAuthentication)) .with(csrf()) @@ -146,7 +146,7 @@ public void testGetComputeSpecConfigByIdNotFound() throws Exception { @Test public void testCreateComputeSpecConfig() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .post("/job-templates/compute-spec-configs") + .post("/compute-spec-configs") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(mapper.writeValueAsString(new ComputeSpecConfig())) @@ -163,7 +163,7 @@ public void testCreateComputeSpecConfig() throws Exception { @Test public void testUpdateComputeSpecConfig() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/job-templates/compute-spec-configs/1") + .put("/compute-spec-configs/1") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(mapper.writeValueAsString(computeSpecConfig)) @@ -182,7 +182,7 @@ public void testUpdateComputeSpecConfig() throws Exception { @Test(expected = NestedServletException.class) public void testUpdateComputeSpecConfig_IdMismatch() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/job-templates/compute-spec-configs/2") + .put("/compute-spec-configs/2") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(mapper.writeValueAsString(computeSpecConfig)) @@ -200,7 +200,7 @@ public void testUpdateComputeSpecConfig_IdMismatch() throws Exception { @Test public void testDeleteComputeSpecConfig() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete("/job-templates/compute-spec-configs/1") + .delete("/compute-spec-configs/1") .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); @@ -214,7 +214,7 @@ public void testDeleteComputeSpecConfig() throws Exception { @Test public void testGetAvailableComputeSpecConfigs() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/job-templates/compute-spec-configs/available") + .get("/compute-spec-configs/available") .param("user", mockUser.getLogin()) .param("project", "projectId") .param("type", "JUPYTERHUB") diff --git a/src/test/java/org/nrg/jobtemplates/rest/ConstraintConfigsApiTest.java b/src/test/java/org/nrg/xnat/compute/rest/ConstraintConfigsApiTest.java similarity index 92% rename from src/test/java/org/nrg/jobtemplates/rest/ConstraintConfigsApiTest.java rename to src/test/java/org/nrg/xnat/compute/rest/ConstraintConfigsApiTest.java index fb39607..9a08639 100644 --- a/src/test/java/org/nrg/jobtemplates/rest/ConstraintConfigsApiTest.java +++ b/src/test/java/org/nrg/xnat/compute/rest/ConstraintConfigsApiTest.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.rest; +package org.nrg.xnat.compute.rest; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.After; @@ -6,9 +6,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.nrg.jobtemplates.config.ConstraintConfigsApiTestConfig; -import org.nrg.jobtemplates.models.ConstraintConfig; -import org.nrg.jobtemplates.services.ConstraintConfigService; +import org.nrg.xnat.compute.config.ConstraintConfigsApiTestConfig; +import org.nrg.xnat.compute.models.ConstraintConfig; +import org.nrg.xnat.compute.services.ConstraintConfigService; import org.nrg.xdat.security.services.RoleServiceI; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xft.security.UserI; @@ -81,7 +81,7 @@ public void after() { public void testGetPlacementConstraintConfig() throws Exception { // Set up the request final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/job-templates/constraint-configs/1") + .get("/constraint-configs/1") .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); @@ -100,7 +100,7 @@ public void testGetPlacementConstraintConfig() throws Exception { public void testGetPlacementConstraintConfigNotFound() throws Exception { // Set up the request final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/job-templates/constraint-configs/1") + .get("/constraint-configs/1") .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); @@ -119,7 +119,7 @@ public void testGetPlacementConstraintConfigNotFound() throws Exception { public void testGetAllPlacementConstraintConfigs() throws Exception { // Set up the request final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/job-templates/constraint-configs") + .get("/constraint-configs") .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); @@ -134,7 +134,7 @@ public void testGetAllPlacementConstraintConfigs() throws Exception { public void testCreatePlacementConstraintConfig() throws Exception { // Set up the request final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .post("/job-templates/constraint-configs") + .post("/constraint-configs") .contentType(APPLICATION_JSON) .accept(APPLICATION_JSON) .with(authentication(mockAuthentication)) @@ -153,7 +153,7 @@ public void testCreatePlacementConstraintConfig() throws Exception { public void testUpdatePlacementConstraintConfig() throws Exception { // Set up the request final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/job-templates/constraint-configs/1") + .put("/constraint-configs/1") .contentType(APPLICATION_JSON) .accept(APPLICATION_JSON) .with(authentication(mockAuthentication)) @@ -172,7 +172,7 @@ public void testUpdatePlacementConstraintConfig() throws Exception { public void testUpdatePlacementConstraintConfig_IdMismatch() throws Exception { // Set up the request final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/job-templates/constraint-configs/2") + .put("/constraint-configs/2") .contentType(APPLICATION_JSON) .accept(APPLICATION_JSON) .with(authentication(mockAuthentication)) @@ -191,7 +191,7 @@ public void testUpdatePlacementConstraintConfig_IdMismatch() throws Exception { public void testDeletePlacementConstraintConfig() throws Exception { // Set up the request final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete("/job-templates/constraint-configs/1") + .delete("/constraint-configs/1") .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); diff --git a/src/test/java/org/nrg/jobtemplates/rest/HardwareConfigsApiTest.java b/src/test/java/org/nrg/xnat/compute/rest/HardwareConfigsApiTest.java similarity index 92% rename from src/test/java/org/nrg/jobtemplates/rest/HardwareConfigsApiTest.java rename to src/test/java/org/nrg/xnat/compute/rest/HardwareConfigsApiTest.java index d132529..1348e96 100644 --- a/src/test/java/org/nrg/jobtemplates/rest/HardwareConfigsApiTest.java +++ b/src/test/java/org/nrg/xnat/compute/rest/HardwareConfigsApiTest.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.rest; +package org.nrg.xnat.compute.rest; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.After; @@ -6,9 +6,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.nrg.jobtemplates.config.HardwareConfigsApiConfig; -import org.nrg.jobtemplates.models.HardwareConfig; -import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xnat.compute.config.HardwareConfigsApiConfig; +import org.nrg.xnat.compute.models.HardwareConfig; +import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xdat.security.services.RoleServiceI; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xft.security.UserI; @@ -82,7 +82,7 @@ public void after() throws Exception { @Test public void testGetAllHardwareConfigs() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/job-templates/hardware-configs") + .get("/hardware-configs") .accept(APPLICATION_JSON) .with(authentication(mockAuthentication)) .with(csrf()) @@ -97,7 +97,7 @@ public void testGetAllHardwareConfigs() throws Exception { @Test public void testGetHardwareConfigById() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/job-templates/hardware-configs/1") + .get("/hardware-configs/1") .accept(APPLICATION_JSON) .with(authentication(mockAuthentication)) .with(csrf()) @@ -114,7 +114,7 @@ public void testGetHardwareConfigById() throws Exception { @Test public void testGetHardwareConfigByIdNotFound() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/job-templates/hardware-configs/1") + .get("/hardware-configs/1") .accept(APPLICATION_JSON) .with(authentication(mockAuthentication)) .with(csrf()) @@ -131,7 +131,7 @@ public void testGetHardwareConfigByIdNotFound() throws Exception { @Test public void testCreateHardwareConfig() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .post("/job-templates/hardware-configs") + .post("/hardware-configs") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(mapper.writeValueAsString(hardwareConfig)) @@ -150,7 +150,7 @@ public void testCreateHardwareConfig() throws Exception { @Test public void testUpdateHardwareConfig() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/job-templates/hardware-configs/1") + .put("/hardware-configs/1") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(mapper.writeValueAsString(hardwareConfig)) @@ -169,7 +169,7 @@ public void testUpdateHardwareConfig() throws Exception { @Test(expected = NestedServletException.class) public void testUpdateHardwareConfig_IdMismatch() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/job-templates/hardware-configs/2") + .put("/hardware-configs/2") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(mapper.writeValueAsString(hardwareConfig)) @@ -186,7 +186,7 @@ public void testUpdateHardwareConfig_IdMismatch() throws Exception { @Test public void testDeleteHardwareConfig() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete("/job-templates/hardware-configs/1") + .delete("/hardware-configs/1") .accept(APPLICATION_JSON) .with(authentication(mockAuthentication)) .with(csrf()) diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigServiceTest.java similarity index 98% rename from src/test/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigServiceTest.java rename to src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigServiceTest.java index 8e493db..7355ff4 100644 --- a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultComputeSpecConfigServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigServiceTest.java @@ -1,14 +1,14 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.nrg.xnat.compute.config.DefaultComputeSpecConfigServiceTestConfig; +import org.nrg.xnat.compute.models.*; import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.jobtemplates.config.DefaultComputeSpecConfigServiceTestConfig; -import org.nrg.jobtemplates.entities.HardwareConfigEntity; -import org.nrg.jobtemplates.models.*; -import org.nrg.jobtemplates.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -21,9 +21,9 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.*; import static org.nrg.framework.constants.Scope.*; -import static org.nrg.jobtemplates.models.ComputeSpecConfig.ConfigType.CONTAINER_SERVICE; -import static org.nrg.jobtemplates.models.ComputeSpecConfig.ConfigType.JUPYTERHUB; -import static org.nrg.jobtemplates.utils.TestingUtils.commitTransaction; +import static org.nrg.xnat.compute.models.ComputeSpecConfig.ConfigType.CONTAINER_SERVICE; +import static org.nrg.xnat.compute.models.ComputeSpecConfig.ConfigType.JUPYTERHUB; +import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; @RunWith(SpringJUnit4ClassRunner.class) @Transactional diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigServiceTest.java similarity index 96% rename from src/test/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigServiceTest.java rename to src/test/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigServiceTest.java index 4439d5f..e1b0381 100644 --- a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultConstraintConfigServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigServiceTest.java @@ -1,14 +1,14 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.jobtemplates.config.DefaultConstraintConfigServiceTestConfig; -import org.nrg.jobtemplates.models.Constraint; -import org.nrg.jobtemplates.models.ConstraintConfig; -import org.nrg.jobtemplates.models.ConstraintScope; +import org.nrg.xnat.compute.config.DefaultConstraintConfigServiceTestConfig; +import org.nrg.xnat.compute.models.Constraint; +import org.nrg.xnat.compute.models.ConstraintConfig; +import org.nrg.xnat.compute.models.ConstraintScope; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -19,7 +19,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; -import static org.nrg.jobtemplates.utils.TestingUtils.commitTransaction; +import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; @RunWith(SpringJUnit4ClassRunner.class) @Transactional diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java similarity index 97% rename from src/test/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigServiceTest.java rename to src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java index 189a3ab..d906940 100644 --- a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultHardwareConfigServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java @@ -1,14 +1,14 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.nrg.xnat.compute.models.*; import org.nrg.framework.constants.Scope; -import org.nrg.jobtemplates.config.DefaultHardwareConfigServiceTestConfig; -import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; -import org.nrg.jobtemplates.entities.HardwareConfigEntity; -import org.nrg.jobtemplates.models.*; -import org.nrg.jobtemplates.services.ComputeSpecConfigEntityService; +import org.nrg.xnat.compute.config.DefaultHardwareConfigServiceTestConfig; +import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -21,7 +21,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import static org.nrg.framework.constants.Scope.*; -import static org.nrg.jobtemplates.utils.TestingUtils.commitTransaction; +import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; @RunWith(SpringJUnit4ClassRunner.class) @Transactional diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java similarity index 95% rename from src/test/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateServiceTest.java rename to src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java index d50035c..eb56d69 100644 --- a/src/test/java/org/nrg/jobtemplates/services/impl/DefaultJobTemplateServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java @@ -1,15 +1,15 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.nrg.jobtemplates.config.DefaultJobTemplateServiceTestConfig; -import org.nrg.jobtemplates.models.*; -import org.nrg.jobtemplates.services.ComputeSpecConfigService; -import org.nrg.jobtemplates.services.ConstraintConfigService; -import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xnat.compute.models.*; +import org.nrg.xnat.compute.config.DefaultJobTemplateServiceTestConfig; +import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.ConstraintConfigService; +import org.nrg.xnat.compute.services.HardwareConfigService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityServiceTest.java similarity index 97% rename from src/test/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityServiceTest.java rename to src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityServiceTest.java index 29ebd94..815b248 100644 --- a/src/test/java/org/nrg/jobtemplates/services/impl/HibernateComputeSpecConfigEntityServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityServiceTest.java @@ -1,17 +1,17 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.nrg.xnat.compute.models.*; import org.nrg.framework.constants.Scope; -import org.nrg.jobtemplates.config.HibernateComputeSpecConfigEntityServiceTestConfig; -import org.nrg.jobtemplates.entities.ComputeSpecConfigEntity; -import org.nrg.jobtemplates.entities.HardwareConfigEntity; -import org.nrg.jobtemplates.models.*; -import org.nrg.jobtemplates.repositories.ComputeSpecConfigDao; -import org.nrg.jobtemplates.repositories.HardwareConfigDao; +import org.nrg.xnat.compute.config.HibernateComputeSpecConfigEntityServiceTestConfig; +import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.repositories.ComputeSpecConfigDao; +import org.nrg.xnat.compute.repositories.HardwareConfigDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.test.annotation.DirtiesContext; @@ -24,9 +24,9 @@ import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; import static org.nrg.framework.constants.Scope.*; -import static org.nrg.jobtemplates.models.ComputeSpecConfig.ConfigType.CONTAINER_SERVICE; -import static org.nrg.jobtemplates.models.ComputeSpecConfig.ConfigType.JUPYTERHUB; -import static org.nrg.jobtemplates.utils.TestingUtils.commitTransaction; +import static org.nrg.xnat.compute.models.ComputeSpecConfig.ConfigType.CONTAINER_SERVICE; +import static org.nrg.xnat.compute.models.ComputeSpecConfig.ConfigType.JUPYTERHUB; +import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; @RunWith(SpringJUnit4ClassRunner.class) @Transactional diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityServiceTest.java similarity index 88% rename from src/test/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityServiceTest.java rename to src/test/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityServiceTest.java index 54c06dd..6c6e872 100644 --- a/src/test/java/org/nrg/jobtemplates/services/impl/HibernateConstraintConfigEntityServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityServiceTest.java @@ -1,13 +1,13 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import org.junit.Test; import org.junit.runner.RunWith; import org.nrg.framework.constants.Scope; -import org.nrg.jobtemplates.config.HibernateConstraintConfigEntityServiceTestConfig; -import org.nrg.jobtemplates.entities.ConstraintConfigEntity; -import org.nrg.jobtemplates.models.Constraint; -import org.nrg.jobtemplates.models.ConstraintConfig; -import org.nrg.jobtemplates.models.ConstraintScope; +import org.nrg.xnat.compute.config.HibernateConstraintConfigEntityServiceTestConfig; +import org.nrg.xnat.compute.entities.ConstraintConfigEntity; +import org.nrg.xnat.compute.models.Constraint; +import org.nrg.xnat.compute.models.ConstraintConfig; +import org.nrg.xnat.compute.models.ConstraintScope; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -21,7 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.nrg.jobtemplates.utils.TestingUtils.commitTransaction; +import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; @RunWith(SpringJUnit4ClassRunner.class) @Transactional diff --git a/src/test/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityServiceTest.java similarity index 86% rename from src/test/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityServiceTest.java rename to src/test/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityServiceTest.java index 73fb54d..8902fd2 100644 --- a/src/test/java/org/nrg/jobtemplates/services/impl/HibernateHardwareConfigEntityServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityServiceTest.java @@ -1,8 +1,8 @@ -package org.nrg.jobtemplates.services.impl; +package org.nrg.xnat.compute.services.impl; import org.junit.Test; import org.junit.runner.RunWith; -import org.nrg.jobtemplates.config.HibernateHardwareConfigEntityServiceTestConfig; +import org.nrg.xnat.compute.config.HibernateHardwareConfigEntityServiceTestConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; diff --git a/src/test/java/org/nrg/jobtemplates/utils/TestingUtils.java b/src/test/java/org/nrg/xnat/compute/utils/TestingUtils.java similarity index 89% rename from src/test/java/org/nrg/jobtemplates/utils/TestingUtils.java rename to src/test/java/org/nrg/xnat/compute/utils/TestingUtils.java index a299000..76a2129 100644 --- a/src/test/java/org/nrg/jobtemplates/utils/TestingUtils.java +++ b/src/test/java/org/nrg/xnat/compute/utils/TestingUtils.java @@ -1,4 +1,4 @@ -package org.nrg.jobtemplates.utils; +package org.nrg.xnat.compute.utils; import lombok.extern.slf4j.Slf4j; import org.springframework.test.context.transaction.TestTransaction; diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java index bb9239a..54518f3 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java @@ -1,7 +1,7 @@ package org.nrg.xnatx.plugins.jupyterhub.config; import org.nrg.framework.services.NrgEventServiceI; -import org.nrg.jobtemplates.services.JobTemplateService; +import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xnatx.plugins.jupyterhub.client.JupyterHubClient; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java index b166292..bc1b664 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java @@ -1,6 +1,6 @@ package org.nrg.xnatx.plugins.jupyterhub.config; -import org.nrg.jobtemplates.services.JobTemplateService; +import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.SearchHelperServiceI; import org.nrg.xdat.services.AliasTokenService; diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java index 5f1fdc0..1ae2e38 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java @@ -1,7 +1,7 @@ package org.nrg.xnatx.plugins.jupyterhub.config; -import org.nrg.jobtemplates.services.ComputeSpecConfigService; -import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnatx.plugins.jupyterhub.initialize.JupyterHubEnvironmentsAndHardwareInitializer; import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java index 20d0798..282aebb 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java @@ -5,9 +5,9 @@ import org.nrg.framework.services.NrgEventServiceI; import org.nrg.framework.services.SerializerService; import org.nrg.framework.utilities.OrderedProperties; -import org.nrg.jobtemplates.services.ComputeSpecConfigService; -import org.nrg.jobtemplates.services.HardwareConfigService; -import org.nrg.jobtemplates.services.JobTemplateService; +import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.HardwareConfigService; +import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.prefs.services.NrgPreferenceService; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.*; diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java index afdaf9a..22f2e15 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java @@ -4,10 +4,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.nrg.jobtemplates.models.ComputeSpecConfig; -import org.nrg.jobtemplates.models.HardwareConfig; -import org.nrg.jobtemplates.services.ComputeSpecConfigService; -import org.nrg.jobtemplates.services.HardwareConfigService; +import org.nrg.xnat.compute.models.ComputeSpecConfig; +import org.nrg.xnat.compute.models.HardwareConfig; +import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xnat.initialization.tasks.InitializingTaskException; import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubEnvironmentsAndHardwareInitializerTestConfig; diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java index 3e73983..9ba9105 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java @@ -8,7 +8,7 @@ import org.mockito.Captor; import org.mockito.Mockito; import org.nrg.framework.services.NrgEventServiceI; -import org.nrg.jobtemplates.services.JobTemplateService; +import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xdat.om.XnatExperimentdata; import org.nrg.xdat.om.XnatImagescandata; import org.nrg.xdat.om.XnatProjectdata; diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java index 998afab..5909f9d 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java @@ -5,7 +5,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.nrg.jobtemplates.models.*; +import org.nrg.xnat.compute.models.*; import org.nrg.xnatx.plugins.jupyterhub.config.DefaultUserOptionsServiceConfig; import org.nrg.xnatx.plugins.jupyterhub.models.docker.TaskTemplate; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; From f4af05d2b704c89a9bb8a452afd247e210844a7c Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Fri, 23 Jun 2023 12:17:56 -0500 Subject: [PATCH 24/49] JHP-63: Rename ComputeSpec to ComputeEnvironment --- ...va => ComputeEnvironmentConfigEntity.java} | 70 +-- ...ity.java => ComputeEnvironmentEntity.java} | 24 +- ...puteEnvironmentHardwareOptionsEntity.java} | 28 +- ...ava => ComputeEnvironmentScopeEntity.java} | 24 +- .../entities/HardwareConfigEntity.java | 10 +- ...mputeSpec.java => ComputeEnvironment.java} | 2 +- ...fig.java => ComputeEnvironmentConfig.java} | 8 +- ...=> ComputeEnvironmentHardwareOptions.java} | 2 +- ...cope.java => ComputeEnvironmentScope.java} | 2 +- .../nrg/xnat/compute/models/JobTemplate.java | 2 +- ....java => ComputeEnvironmentConfigDao.java} | 40 +- .../repositories/HardwareConfigDao.java | 4 +- .../rest/ComputeEnvironmentConfigsApi.java | 129 +++++ .../compute/rest/ComputeSpecConfigsApi.java | 129 ----- ...ComputeEnvironmentConfigEntityService.java | 15 + .../ComputeEnvironmentConfigService.java | 22 + .../ComputeSpecConfigEntityService.java | 15 - .../services/ComputeSpecConfigService.java | 22 - .../compute/services/JobTemplateService.java | 4 +- ...efaultComputeEnvironmentConfigService.java | 397 ++++++++++++++ .../impl/DefaultComputeSpecConfigService.java | 397 -------------- .../impl/DefaultHardwareConfigService.java | 30 +- .../impl/DefaultJobTemplateService.java | 58 +- ...ComputeEnvironmentConfigEntityService.java | 77 +++ ...bernateComputeSpecConfigEntityService.java | 77 --- ...HubEnvironmentsAndHardwareInitializer.java | 68 +-- .../jupyterhub/models/ServerStartRequest.java | 2 +- .../services/UserOptionsService.java | 2 +- .../impl/DefaultJupyterHubService.java | 12 +- .../impl/DefaultUserOptionsService.java | 24 +- ...figs.js => compute-environment-configs.js} | 146 ++--- .../plugin/jupyterhub/jupyterhub-servers.js | 28 +- .../screens/topBar/Jupyter/Default.vm | 2 +- .../spawner/jupyterhub/site-settings.yaml | 12 +- ...> ComputeEnvironmentConfigsApiConfig.java} | 20 +- ...uteEnvironmentConfigServiceTestConfig.java | 23 + ...ultComputeSpecConfigServiceTestConfig.java | 23 - ...efaultHardwareConfigServiceTestConfig.java | 6 +- .../DefaultJobTemplateServiceTestConfig.java | 6 +- ...ronmentConfigEntityServiceTestConfig.java} | 30 +- .../xnat/compute/config/HibernateConfig.java | 8 +- ...nstraintConfigEntityServiceTestConfig.java | 8 +- .../config/HibernateEntityServicesConfig.java | 20 +- ...HardwareConfigEntityServiceTestConfig.java | 8 +- .../nrg/xnat/compute/config/MockConfig.java | 18 +- ... => ComputeEnvironmentConfigsApiTest.java} | 92 ++-- ...tComputeEnvironmentConfigServiceTest.java} | 340 ++++++------ .../DefaultHardwareConfigServiceTest.java | 90 ++-- .../impl/DefaultJobTemplateServiceTest.java | 68 +-- ...uteEnvironmentConfigEntityServiceTest.java | 505 ++++++++++++++++++ ...ateComputeSpecConfigEntityServiceTest.java | 505 ------------------ ...mentsAndHardwareInitializerTestConfig.java | 6 +- .../plugins/jupyterhub/config/MockConfig.java | 6 +- ...nvironmentsAndHardwareInitializerTest.java | 18 +- .../jupyterhub/rest/JupyterHubApiTest.java | 2 +- .../impl/DefaultJupyterHubServiceTest.java | 16 +- .../impl/DefaultUserOptionsServiceTest.java | 16 +- 57 files changed, 1860 insertions(+), 1858 deletions(-) rename src/main/java/org/nrg/xnat/compute/entities/{ComputeSpecConfigEntity.java => ComputeEnvironmentConfigEntity.java} (55%) rename src/main/java/org/nrg/xnat/compute/entities/{ComputeSpecEntity.java => ComputeEnvironmentEntity.java} (82%) rename src/main/java/org/nrg/xnat/compute/entities/{ComputeSpecHardwareOptionsEntity.java => ComputeEnvironmentHardwareOptionsEntity.java} (70%) rename src/main/java/org/nrg/xnat/compute/entities/{ComputeSpecScopeEntity.java => ComputeEnvironmentScopeEntity.java} (71%) rename src/main/java/org/nrg/xnat/compute/models/{ComputeSpec.java => ComputeEnvironment.java} (94%) rename src/main/java/org/nrg/xnat/compute/models/{ComputeSpecConfig.java => ComputeEnvironmentConfig.java} (64%) rename src/main/java/org/nrg/xnat/compute/models/{ComputeSpecHardwareOptions.java => ComputeEnvironmentHardwareOptions.java} (86%) rename src/main/java/org/nrg/xnat/compute/models/{ComputeSpecScope.java => ComputeEnvironmentScope.java} (93%) rename src/main/java/org/nrg/xnat/compute/repositories/{ComputeSpecConfigDao.java => ComputeEnvironmentConfigDao.java} (61%) create mode 100644 src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java delete mode 100644 src/main/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApi.java create mode 100644 src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigEntityService.java create mode 100644 src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigEntityService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigService.java create mode 100644 src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigService.java create mode 100644 src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityService.java rename src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/{compute-spec-configs.js => compute-environment-configs.js} (88%) rename src/test/java/org/nrg/xnat/compute/config/{ComputeSpecConfigsApiConfig.java => ComputeEnvironmentConfigsApiConfig.java} (65%) create mode 100644 src/test/java/org/nrg/xnat/compute/config/DefaultComputeEnvironmentConfigServiceTestConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/DefaultComputeSpecConfigServiceTestConfig.java rename src/test/java/org/nrg/xnat/compute/config/{HibernateComputeSpecConfigEntityServiceTestConfig.java => HibernateComputeEnvironmentConfigEntityServiceTestConfig.java} (63%) rename src/test/java/org/nrg/xnat/compute/rest/{ComputeSpecConfigsApiTest.java => ComputeEnvironmentConfigsApiTest.java} (66%) rename src/test/java/org/nrg/xnat/compute/services/impl/{DefaultComputeSpecConfigServiceTest.java => DefaultComputeEnvironmentConfigServiceTest.java} (55%) create mode 100644 src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityServiceTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityServiceTest.java diff --git a/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecConfigEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentConfigEntity.java similarity index 55% rename from src/main/java/org/nrg/xnat/compute/entities/ComputeSpecConfigEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentConfigEntity.java index 6f30f80..e8990f8 100644 --- a/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecConfigEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentConfigEntity.java @@ -3,7 +3,7 @@ import lombok.*; import org.nrg.framework.constants.Scope; import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.xnat.compute.models.ComputeSpecConfig; +import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; import javax.persistence.*; import java.util.HashSet; @@ -17,13 +17,13 @@ @NoArgsConstructor @ToString @EqualsAndHashCode(callSuper = true) -public class ComputeSpecConfigEntity extends AbstractHibernateEntity { +public class ComputeEnvironmentConfigEntity extends AbstractHibernateEntity { private Set configTypes; - private ComputeSpecEntity computeSpec; - private Map scopes; - private ComputeSpecHardwareOptionsEntity hardwareOptions; + private ComputeEnvironmentEntity computeEnvironment; + private Map scopes; + private ComputeEnvironmentHardwareOptionsEntity hardwareOptions; @ElementCollection public Set getConfigTypes() { @@ -38,31 +38,31 @@ public void setConfigTypes(Set configTypes) { this.configTypes = configTypes; } - @OneToOne(mappedBy = "computeSpecConfig", cascade = CascadeType.ALL, orphanRemoval = true) - public ComputeSpecEntity getComputeSpec() { - return computeSpec; + @OneToOne(mappedBy = "computeEnvironmentConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public ComputeEnvironmentEntity getComputeEnvironment() { + return computeEnvironment; } - public void setComputeSpec(ComputeSpecEntity computeSpec) { - computeSpec.setComputeSpecConfig(this); - this.computeSpec = computeSpec; + public void setComputeEnvironment(ComputeEnvironmentEntity computeEnvironment) { + computeEnvironment.setComputeEnvironmentConfig(this); + this.computeEnvironment = computeEnvironment; } - @OneToMany(mappedBy = "computeSpecConfig", cascade = CascadeType.ALL, orphanRemoval = true) - public Map getScopes() { + @OneToMany(mappedBy = "computeEnvironmentConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public Map getScopes() { return scopes; } - public void setScopes(Map scopes) { + public void setScopes(Map scopes) { this.scopes = scopes; } - @OneToOne(mappedBy = "computeSpecConfig", cascade = CascadeType.ALL, orphanRemoval = true) - public ComputeSpecHardwareOptionsEntity getHardwareOptions() { + @OneToOne(mappedBy = "computeEnvironmentConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public ComputeEnvironmentHardwareOptionsEntity getHardwareOptions() { return hardwareOptions; } - public void setHardwareOptions(ComputeSpecHardwareOptionsEntity hardwareOptions) { + public void setHardwareOptions(ComputeEnvironmentHardwareOptionsEntity hardwareOptions) { this.hardwareOptions = hardwareOptions; } @@ -71,8 +71,8 @@ public void setHardwareOptions(ComputeSpecHardwareOptionsEntity hardwareOptions) * @param pojo The pojo to create the entity from * @return The newly created entity */ - public static ComputeSpecConfigEntity fromPojo(final ComputeSpecConfig pojo) { - final ComputeSpecConfigEntity entity = new ComputeSpecConfigEntity(); + public static ComputeEnvironmentConfigEntity fromPojo(final ComputeEnvironmentConfig pojo) { + final ComputeEnvironmentConfigEntity entity = new ComputeEnvironmentConfigEntity(); entity.update(pojo); return entity; } @@ -81,14 +81,14 @@ public static ComputeSpecConfigEntity fromPojo(final ComputeSpecConfig pojo) { * Creates a new pojo from the entity. * @return The pojo created from the entity */ - public ComputeSpecConfig toPojo() { - return ComputeSpecConfig.builder() + public ComputeEnvironmentConfig toPojo() { + return ComputeEnvironmentConfig.builder() .id(getId()) .configTypes(getConfigTypes() .stream() - .map(ComputeSpecConfig.ConfigType::valueOf) + .map(ComputeEnvironmentConfig.ConfigType::valueOf) .collect(Collectors.toSet())) - .computeSpec(getComputeSpec().toPojo()) + .computeEnvironment(getComputeEnvironment().toPojo()) .scopes(getScopes() .entrySet() .stream() @@ -102,18 +102,18 @@ public ComputeSpecConfig toPojo() { * many-to-many relationship and needs to be handled separately. * @param pojo The pojo to update the entity with */ - public void update(final ComputeSpecConfig pojo) { + public void update(final ComputeEnvironmentConfig pojo) { setConfigTypes(pojo.getConfigTypes() .stream() .map(Enum::name) .collect(Collectors.toSet())); - if (getComputeSpec() == null) { - // This is a new entity, so we need to create the computeSpec entity - setComputeSpec(ComputeSpecEntity.fromPojo(pojo.getComputeSpec())); + if (getComputeEnvironment() == null) { + // This is a new entity, so we need to create the computeEnvironment entity + setComputeEnvironment(ComputeEnvironmentEntity.fromPojo(pojo.getComputeEnvironment())); } else { - // This is an existing entity, so we need to update the computeSpec entity - getComputeSpec().update(pojo.getComputeSpec()); + // This is an existing entity, so we need to update the computeEnvironment entity + getComputeEnvironment().update(pojo.getComputeEnvironment()); } if (getScopes() == null) { @@ -121,20 +121,20 @@ public void update(final ComputeSpecConfig pojo) { setScopes(pojo.getScopes() .entrySet() .stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> ComputeSpecScopeEntity.fromPojo(e.getValue())))); + .collect(Collectors.toMap(Map.Entry::getKey, e -> ComputeEnvironmentScopeEntity.fromPojo(e.getValue())))); } else { // This is an existing entity, so we need to update the scopes entities getScopes().forEach((key, value) -> value.update(pojo.getScopes().get(key))); } - // Set the computeSpecConfig on the scopes - getScopes().values().forEach(s -> s.setComputeSpecConfig(this)); + // Set the computeEnvironmentConfig on the scopes + getScopes().values().forEach(s -> s.setComputeEnvironmentConfig(this)); if (getHardwareOptions() == null) { // This is a new entity, so we need to create the hardwareOptions entity - ComputeSpecHardwareOptionsEntity computeSpecHardwareOptionsEntity = ComputeSpecHardwareOptionsEntity.fromPojo(pojo.getHardwareOptions()); - setHardwareOptions(computeSpecHardwareOptionsEntity); - computeSpecHardwareOptionsEntity.setComputeSpecConfig(this); + ComputeEnvironmentHardwareOptionsEntity computeEnvironmentHardwareOptionsEntity = ComputeEnvironmentHardwareOptionsEntity.fromPojo(pojo.getHardwareOptions()); + setHardwareOptions(computeEnvironmentHardwareOptionsEntity); + computeEnvironmentHardwareOptionsEntity.setComputeEnvironmentConfig(this); } else { // This is an existing entity, so we need to update the hardwareOptions entity getHardwareOptions().update(pojo.getHardwareOptions()); diff --git a/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentEntity.java similarity index 82% rename from src/main/java/org/nrg/xnat/compute/entities/ComputeSpecEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentEntity.java index 5c094ea..5c0e110 100644 --- a/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentEntity.java @@ -2,7 +2,7 @@ import lombok.*; import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.xnat.compute.models.ComputeSpec; +import org.nrg.xnat.compute.models.ComputeEnvironment; import javax.persistence.*; import java.util.ArrayList; @@ -16,7 +16,7 @@ @NoArgsConstructor @ToString @EqualsAndHashCode(callSuper = true) -public class ComputeSpecEntity extends AbstractHibernateEntity { +public class ComputeEnvironmentEntity extends AbstractHibernateEntity { private String name; private String image; @@ -25,7 +25,7 @@ public class ComputeSpecEntity extends AbstractHibernateEntity { private List environmentVariables; private List mounts; - @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeSpecConfigEntity computeSpecConfig; + @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeEnvironmentConfigEntity computeEnvironmentConfig; public String getName() { return name; @@ -78,12 +78,12 @@ public void setMounts(List mounts) { } @OneToOne - public ComputeSpecConfigEntity getComputeSpecConfig() { - return computeSpecConfig; + public ComputeEnvironmentConfigEntity getComputeEnvironmentConfig() { + return computeEnvironmentConfig; } - public void setComputeSpecConfig(ComputeSpecConfigEntity computeSpecConfig) { - this.computeSpecConfig = computeSpecConfig; + public void setComputeEnvironmentConfig(ComputeEnvironmentConfigEntity computeEnvironmentConfig) { + this.computeEnvironmentConfig = computeEnvironmentConfig; } /** @@ -91,8 +91,8 @@ public void setComputeSpecConfig(ComputeSpecConfigEntity computeSpecConfig) { * @param pojo The pojo to convert. * @return The entity created from the pojo. */ - public static ComputeSpecEntity fromPojo(ComputeSpec pojo) { - final ComputeSpecEntity entity = new ComputeSpecEntity(); + public static ComputeEnvironmentEntity fromPojo(ComputeEnvironment pojo) { + final ComputeEnvironmentEntity entity = new ComputeEnvironmentEntity(); entity.update(pojo); return entity; } @@ -101,8 +101,8 @@ public static ComputeSpecEntity fromPojo(ComputeSpec pojo) { * Converts this entity to a pojo. * @return The pojo created from this entity. */ - public ComputeSpec toPojo() { - return ComputeSpec.builder() + public ComputeEnvironment toPojo() { + return ComputeEnvironment.builder() .name(getName()) .image(getImage()) .command(getCommand()) @@ -115,7 +115,7 @@ public ComputeSpec toPojo() { * Updates this entity from the given pojo. * @param pojo The pojo to update from. */ - public void update(ComputeSpec pojo) { + public void update(ComputeEnvironment pojo) { setName(pojo.getName()); setImage(pojo.getImage()); setCommand(pojo.getCommand()); diff --git a/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecHardwareOptionsEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentHardwareOptionsEntity.java similarity index 70% rename from src/main/java/org/nrg/xnat/compute/entities/ComputeSpecHardwareOptionsEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentHardwareOptionsEntity.java index b7e34b0..c3c5ee9 100644 --- a/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecHardwareOptionsEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentHardwareOptionsEntity.java @@ -1,7 +1,7 @@ package org.nrg.xnat.compute.entities; import lombok.*; -import org.nrg.xnat.compute.models.ComputeSpecHardwareOptions; +import org.nrg.xnat.compute.models.ComputeEnvironmentHardwareOptions; import javax.persistence.*; import java.util.HashSet; @@ -14,11 +14,11 @@ @NoArgsConstructor @ToString @EqualsAndHashCode -public class ComputeSpecHardwareOptionsEntity { +public class ComputeEnvironmentHardwareOptionsEntity { private long id; - @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeSpecConfigEntity computeSpecConfig; + @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeEnvironmentConfigEntity computeEnvironmentConfig; private boolean allowAllHardware; private Set hardwareConfigs; @@ -34,12 +34,12 @@ public void setId(long id) { @OneToOne @MapsId - public ComputeSpecConfigEntity getComputeSpecConfig() { - return computeSpecConfig; + public ComputeEnvironmentConfigEntity getComputeEnvironmentConfig() { + return computeEnvironmentConfig; } - public void setComputeSpecConfig(ComputeSpecConfigEntity computeSpecConfig) { - this.computeSpecConfig = computeSpecConfig; + public void setComputeEnvironmentConfig(ComputeEnvironmentConfigEntity computeEnvironmentConfig) { + this.computeEnvironmentConfig = computeEnvironmentConfig; } public boolean isAllowAllHardware() { @@ -51,8 +51,8 @@ public void setAllowAllHardware(boolean allowAllHardware) { } @ManyToMany - @JoinTable(name = "compute_spec_hardware_options_hardware_config", - joinColumns = @JoinColumn(name = "compute_spec_hardware_options_id"), + @JoinTable(name = "compute_environment_hardware_options_hardware_config", + joinColumns = @JoinColumn(name = "compute_environment_hardware_options_id"), inverseJoinColumns = @JoinColumn(name = "hardware_config_id")) public Set getHardwareConfigs() { return hardwareConfigs; @@ -81,8 +81,8 @@ public void removeHardwareConfig(HardwareConfigEntity hardwareConfig) { * @param pojo The pojo to create the entity from. * @return The newly created entity. */ - public static ComputeSpecHardwareOptionsEntity fromPojo(final ComputeSpecHardwareOptions pojo) { - final ComputeSpecHardwareOptionsEntity entity = new ComputeSpecHardwareOptionsEntity(); + public static ComputeEnvironmentHardwareOptionsEntity fromPojo(final ComputeEnvironmentHardwareOptions pojo) { + final ComputeEnvironmentHardwareOptionsEntity entity = new ComputeEnvironmentHardwareOptionsEntity(); entity.update(pojo); return entity; } @@ -91,8 +91,8 @@ public static ComputeSpecHardwareOptionsEntity fromPojo(final ComputeSpecHardwar * Creates a new pojo from the given entity. * @return The newly created pojo. */ - public ComputeSpecHardwareOptions toPojo() { - return ComputeSpecHardwareOptions.builder() + public ComputeEnvironmentHardwareOptions toPojo() { + return ComputeEnvironmentHardwareOptions.builder() .allowAllHardware(allowAllHardware) .hardwareConfigs(hardwareConfigs.stream().map(HardwareConfigEntity::toPojo).collect(Collectors.toSet())) .build(); @@ -102,7 +102,7 @@ public ComputeSpecHardwareOptions toPojo() { * Updates the entity with the values from the given pojo. * @param pojo The pojo to update the entity with. */ - public void update(final ComputeSpecHardwareOptions pojo) { + public void update(final ComputeEnvironmentHardwareOptions pojo) { setAllowAllHardware(pojo.isAllowAllHardware()); if (hardwareConfigs == null) { diff --git a/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecScopeEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentScopeEntity.java similarity index 71% rename from src/main/java/org/nrg/xnat/compute/entities/ComputeSpecScopeEntity.java rename to src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentScopeEntity.java index 4facacb..4a7ef9c 100644 --- a/src/main/java/org/nrg/xnat/compute/entities/ComputeSpecScopeEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentScopeEntity.java @@ -2,7 +2,7 @@ import lombok.*; import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.models.ComputeSpecScope; +import org.nrg.xnat.compute.models.ComputeEnvironmentScope; import javax.persistence.*; import java.util.HashSet; @@ -14,14 +14,14 @@ @NoArgsConstructor @ToString @EqualsAndHashCode -public class ComputeSpecScopeEntity { +public class ComputeEnvironmentScopeEntity { private long id; private String scope; private boolean enabled; private Set ids; - @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeSpecConfigEntity computeSpecConfig; + @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeEnvironmentConfigEntity computeEnvironmentConfig; @Id @GeneratedValue @@ -63,19 +63,19 @@ public void setIds(Set ids) { } @ManyToOne - public ComputeSpecConfigEntity getComputeSpecConfig() { - return computeSpecConfig; + public ComputeEnvironmentConfigEntity getComputeEnvironmentConfig() { + return computeEnvironmentConfig; } - public void setComputeSpecConfig(ComputeSpecConfigEntity computeSpecConfig) { - this.computeSpecConfig = computeSpecConfig; + public void setComputeEnvironmentConfig(ComputeEnvironmentConfigEntity computeEnvironmentConfig) { + this.computeEnvironmentConfig = computeEnvironmentConfig; } /** * Updates this entity with the values from the given pojo. * @param pojo The pojo to update from */ - public void update(ComputeSpecScope pojo) { + public void update(ComputeEnvironmentScope pojo) { setScope(pojo.getScope().name()); setEnabled(pojo.isEnabled()); @@ -91,8 +91,8 @@ public void update(ComputeSpecScope pojo) { * Converts this entity to a pojo. * @return The pojo */ - public ComputeSpecScope toPojo() { - return ComputeSpecScope.builder() + public ComputeEnvironmentScope toPojo() { + return ComputeEnvironmentScope.builder() .scope(Scope.valueOf(getScope())) .enabled(isEnabled()) .ids(getIds()) @@ -104,8 +104,8 @@ public ComputeSpecScope toPojo() { * @param pojo The pojo to create from * @return The new entity */ - public static ComputeSpecScopeEntity fromPojo(ComputeSpecScope pojo) { - final ComputeSpecScopeEntity entity = new ComputeSpecScopeEntity(); + public static ComputeEnvironmentScopeEntity fromPojo(ComputeEnvironmentScope pojo) { + final ComputeEnvironmentScopeEntity entity = new ComputeEnvironmentScopeEntity(); entity.update(pojo); return entity; } diff --git a/src/main/java/org/nrg/xnat/compute/entities/HardwareConfigEntity.java b/src/main/java/org/nrg/xnat/compute/entities/HardwareConfigEntity.java index 75c37c0..3c250a1 100644 --- a/src/main/java/org/nrg/xnat/compute/entities/HardwareConfigEntity.java +++ b/src/main/java/org/nrg/xnat/compute/entities/HardwareConfigEntity.java @@ -21,7 +21,7 @@ public class HardwareConfigEntity extends AbstractHibernateEntity { private HardwareEntity hardware; private Map scopes; - @ToString.Exclude @EqualsAndHashCode.Exclude private List computeSpecHardwareOptions; + @ToString.Exclude @EqualsAndHashCode.Exclude private List computeEnvironmentHardwareOptions; @OneToOne(mappedBy = "hardwareConfig", cascade = CascadeType.ALL, orphanRemoval = true) public HardwareEntity getHardware() { @@ -43,12 +43,12 @@ public void setScopes(Map scopes) { } @ManyToMany(mappedBy = "hardwareConfigs") - public List getComputeSpecHardwareOptions() { - return computeSpecHardwareOptions; + public List getComputeEnvironmentHardwareOptions() { + return computeEnvironmentHardwareOptions; } - public void setComputeSpecHardwareOptions(List computeSpecHardwareOptions) { - this.computeSpecHardwareOptions = computeSpecHardwareOptions; + public void setComputeEnvironmentHardwareOptions(List computeEnvironmentHardwareOptions) { + this.computeEnvironmentHardwareOptions = computeEnvironmentHardwareOptions; } /** diff --git a/src/main/java/org/nrg/xnat/compute/models/ComputeSpec.java b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironment.java similarity index 94% rename from src/main/java/org/nrg/xnat/compute/models/ComputeSpec.java rename to src/main/java/org/nrg/xnat/compute/models/ComputeEnvironment.java index 30acca7..ae4145b 100644 --- a/src/main/java/org/nrg/xnat/compute/models/ComputeSpec.java +++ b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironment.java @@ -12,7 +12,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class ComputeSpec { +public class ComputeEnvironment { @ApiModelProperty(position = 0) private String name; @ApiModelProperty(position = 1) private String image; diff --git a/src/main/java/org/nrg/xnat/compute/models/ComputeSpecConfig.java b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentConfig.java similarity index 64% rename from src/main/java/org/nrg/xnat/compute/models/ComputeSpecConfig.java rename to src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentConfig.java index 631402c..c92a681 100644 --- a/src/main/java/org/nrg/xnat/compute/models/ComputeSpecConfig.java +++ b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentConfig.java @@ -14,13 +14,13 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class ComputeSpecConfig { +public class ComputeEnvironmentConfig { @ApiModelProperty(position = 0) private Long id; @ApiModelProperty(position = 1) private Set configTypes; - @ApiModelProperty(position = 2) private ComputeSpec computeSpec; - @ApiModelProperty(position = 3) private Map scopes; - @ApiModelProperty(position = 4) private ComputeSpecHardwareOptions hardwareOptions; + @ApiModelProperty(position = 2) private ComputeEnvironment computeEnvironment; + @ApiModelProperty(position = 3) private Map scopes; + @ApiModelProperty(position = 4) private ComputeEnvironmentHardwareOptions hardwareOptions; public enum ConfigType { JUPYTERHUB, diff --git a/src/main/java/org/nrg/xnat/compute/models/ComputeSpecHardwareOptions.java b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentHardwareOptions.java similarity index 86% rename from src/main/java/org/nrg/xnat/compute/models/ComputeSpecHardwareOptions.java rename to src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentHardwareOptions.java index 8a80f26..adf57fe 100644 --- a/src/main/java/org/nrg/xnat/compute/models/ComputeSpecHardwareOptions.java +++ b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentHardwareOptions.java @@ -11,7 +11,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class ComputeSpecHardwareOptions { +public class ComputeEnvironmentHardwareOptions { private boolean allowAllHardware; private Set hardwareConfigs; diff --git a/src/main/java/org/nrg/xnat/compute/models/ComputeSpecScope.java b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentScope.java similarity index 93% rename from src/main/java/org/nrg/xnat/compute/models/ComputeSpecScope.java rename to src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentScope.java index 880f1c8..b71792e 100644 --- a/src/main/java/org/nrg/xnat/compute/models/ComputeSpecScope.java +++ b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentScope.java @@ -12,7 +12,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class ComputeSpecScope { +public class ComputeEnvironmentScope { private Scope scope; private boolean enabled; // For project and user scopes, this is whether the scope is enabled for all projects or users diff --git a/src/main/java/org/nrg/xnat/compute/models/JobTemplate.java b/src/main/java/org/nrg/xnat/compute/models/JobTemplate.java index a6a3a64..85eee12 100644 --- a/src/main/java/org/nrg/xnat/compute/models/JobTemplate.java +++ b/src/main/java/org/nrg/xnat/compute/models/JobTemplate.java @@ -13,7 +13,7 @@ @Data public class JobTemplate { - private ComputeSpec computeSpec; + private ComputeEnvironment computeEnvironment; private Hardware hardware; private List constraints; diff --git a/src/main/java/org/nrg/xnat/compute/repositories/ComputeSpecConfigDao.java b/src/main/java/org/nrg/xnat/compute/repositories/ComputeEnvironmentConfigDao.java similarity index 61% rename from src/main/java/org/nrg/xnat/compute/repositories/ComputeSpecConfigDao.java rename to src/main/java/org/nrg/xnat/compute/repositories/ComputeEnvironmentConfigDao.java index a9d8bcf..21c9de0 100644 --- a/src/main/java/org/nrg/xnat/compute/repositories/ComputeSpecConfigDao.java +++ b/src/main/java/org/nrg/xnat/compute/repositories/ComputeEnvironmentConfigDao.java @@ -6,8 +6,8 @@ import org.hibernate.SessionFactory; import org.hibernate.criterion.Restrictions; import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; -import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; -import org.nrg.xnat.compute.entities.ComputeSpecScopeEntity; +import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; +import org.nrg.xnat.compute.entities.ComputeEnvironmentScopeEntity; import org.nrg.xnat.compute.entities.HardwareConfigEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; @@ -17,19 +17,19 @@ @Repository @Slf4j -public class ComputeSpecConfigDao extends AbstractHibernateDAO { +public class ComputeEnvironmentConfigDao extends AbstractHibernateDAO { private final HardwareConfigDao hardwareConfigDao; // For testing - public ComputeSpecConfigDao(final SessionFactory sessionFactory, - final HardwareConfigDao hardwareConfigDao) { + public ComputeEnvironmentConfigDao(final SessionFactory sessionFactory, + final HardwareConfigDao hardwareConfigDao) { super(sessionFactory); this.hardwareConfigDao = hardwareConfigDao; } @Autowired - public ComputeSpecConfigDao(HardwareConfigDao hardwareConfigDao) { + public ComputeEnvironmentConfigDao(HardwareConfigDao hardwareConfigDao) { this.hardwareConfigDao = hardwareConfigDao; } @@ -38,7 +38,7 @@ public ComputeSpecConfigDao(HardwareConfigDao hardwareConfigDao) { * @param entity The entity to initialize. */ @Override - public void initialize(final ComputeSpecConfigEntity entity) { + public void initialize(final ComputeEnvironmentConfigEntity entity) { if (entity == null) { return; } @@ -47,16 +47,16 @@ public void initialize(final ComputeSpecConfigEntity entity) { Hibernate.initialize(entity.getConfigTypes()); - Hibernate.initialize(entity.getComputeSpec()); - if (entity.getComputeSpec() != null) { - Hibernate.initialize(entity.getComputeSpec().getEnvironmentVariables()); - Hibernate.initialize(entity.getComputeSpec().getMounts()); + Hibernate.initialize(entity.getComputeEnvironment()); + if (entity.getComputeEnvironment() != null) { + Hibernate.initialize(entity.getComputeEnvironment().getEnvironmentVariables()); + Hibernate.initialize(entity.getComputeEnvironment().getMounts()); } Hibernate.initialize(entity.getScopes()); if (entity.getScopes() != null) { - entity.getScopes().forEach((scope, computeSpecScopeEntity) -> { - initialize(computeSpecScopeEntity); + entity.getScopes().forEach((scope, computeEnvironmentScopeEntity) -> { + initialize(computeEnvironmentScopeEntity); }); } @@ -76,7 +76,7 @@ public void initialize(final ComputeSpecConfigEntity entity) { * Initializes the entity, loading all collections and proxies. * @param entity The entity to initialize. */ - public void initialize(ComputeSpecScopeEntity entity) { + public void initialize(ComputeEnvironmentScopeEntity entity) { if (entity == null) { return; } @@ -86,23 +86,23 @@ public void initialize(ComputeSpecScopeEntity entity) { } /** - * Finds all compute spec configs that have the specified type. + * Finds all compute environment configs that have the specified type. * @param type The type to search for. - * @return The list of compute spec configs that have the specified type. + * @return The list of compute environment configs that have the specified type. */ - public List findByType(String type) { + public List findByType(String type) { // Need to use a criteria query because the configTypes field is a collection. - Criteria criteria = getSession().createCriteria(ComputeSpecConfigEntity.class) + Criteria criteria = getSession().createCriteria(ComputeEnvironmentConfigEntity.class) .createCriteria("configTypes") .add(Restrictions.eq("elements", type)); - List entities = criteria.list(); + List entities = criteria.list(); if (entities == null) { return Collections.emptyList(); } - for (ComputeSpecConfigEntity entity : entities) { + for (ComputeEnvironmentConfigEntity entity : entities) { initialize(entity); } diff --git a/src/main/java/org/nrg/xnat/compute/repositories/HardwareConfigDao.java b/src/main/java/org/nrg/xnat/compute/repositories/HardwareConfigDao.java index a81ce5d..b9ef99c 100644 --- a/src/main/java/org/nrg/xnat/compute/repositories/HardwareConfigDao.java +++ b/src/main/java/org/nrg/xnat/compute/repositories/HardwareConfigDao.java @@ -43,8 +43,8 @@ public void initialize(final HardwareConfigEntity entity) { initialize(entity.getHardware()); } - if (entity.getComputeSpecHardwareOptions() != null) { - Hibernate.initialize(entity.getComputeSpecHardwareOptions()); + if (entity.getComputeEnvironmentHardwareOptions() != null) { + Hibernate.initialize(entity.getComputeEnvironmentHardwareOptions()); } } diff --git a/src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java b/src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java new file mode 100644 index 0000000..e1528da --- /dev/null +++ b/src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java @@ -0,0 +1,129 @@ +package org.nrg.xnat.compute.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import org.nrg.framework.annotations.XapiRestController; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.xapi.rest.AbstractXapiRestController; +import org.nrg.xapi.rest.XapiRequestMapping; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static org.nrg.xdat.security.helpers.AccessLevel.Admin; +import static org.nrg.xdat.security.helpers.AccessLevel.Read; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@Api("ComputeEnvironmentConfigs REST API") +@XapiRestController +@RequestMapping(value = "/compute-environment-configs") +public class ComputeEnvironmentConfigsApi extends AbstractXapiRestController { + + private final ComputeEnvironmentConfigService computeEnvironmentConfigService; + + @Autowired + public ComputeEnvironmentConfigsApi(final UserManagementServiceI userManagementService, + final RoleHolder roleHolder, + final ComputeEnvironmentConfigService computeEnvironmentConfigService) { + super(userManagementService, roleHolder); + this.computeEnvironmentConfigService = computeEnvironmentConfigService; + } + + @ApiOperation(value = "Get all compute environment configs or all compute environment configs for a given type.", response = ComputeEnvironmentConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Compute environment configs successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Admin) + public List getAll(@RequestParam(value = "type", required = false) final ComputeEnvironmentConfig.ConfigType type) { + if (type != null) { + return computeEnvironmentConfigService.getByType(type); + } else { + return computeEnvironmentConfigService.getAll(); + } + } + + @ApiOperation(value = "Get a compute environment config.", response = ComputeEnvironmentConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Compute environment config successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Compute environment config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Admin) + public ComputeEnvironmentConfig get(@PathVariable("id") final Long id) throws NotFoundException { + return computeEnvironmentConfigService.retrieve(id) + .orElseThrow(() -> new NotFoundException("Compute environment config not found.")); + } + + @ApiOperation(value = "Create a compute environment config.", response = ComputeEnvironmentConfig.class) + @ApiResponses({ + @ApiResponse(code = 201, message = "Compute environment config successfully created."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.CREATED) + @XapiRequestMapping(value = "",consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.POST, restrictTo = Admin) + public ComputeEnvironmentConfig create(@RequestBody final ComputeEnvironmentConfig computeEnvironmentConfig) { + return computeEnvironmentConfigService.create(computeEnvironmentConfig); + } + + @ApiOperation(value = "Update a compute environment config.", response = ComputeEnvironmentConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Compute environment config successfully updated."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Compute environment config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}",consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.PUT, restrictTo = Admin) + public ComputeEnvironmentConfig update(@PathVariable("id") final Long id, + @RequestBody final ComputeEnvironmentConfig computeEnvironmentConfig) throws NotFoundException { + if (!id.equals(computeEnvironmentConfig.getId())) { + throw new IllegalArgumentException("The ID in the path must match the ID in the body."); + } + + return computeEnvironmentConfigService.update(computeEnvironmentConfig); + } + + @ApiOperation(value = "Delete a compute environment config.") + @ApiResponses({ + @ApiResponse(code = 204, message = "Compute environment config successfully deleted."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Compute environment config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + @XapiRequestMapping(value = "/{id}", method = RequestMethod.DELETE, restrictTo = Admin) + public void delete(@PathVariable("id") final Long id) throws NotFoundException { + computeEnvironmentConfigService.delete(id); + } + + @ApiOperation(value = "Get all available compute environment configs for the given user and project.", response = ComputeEnvironmentConfig.class, responseContainer = "List") + @ApiResponses({ + @ApiResponse(code = 200, message = "Compute environment configs successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/available", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Read) + public List getAvailable(@RequestParam(value = "user") final String user, + @RequestParam(value = "project") final String project, + @RequestParam(value = "type", required = false) final ComputeEnvironmentConfig.ConfigType type) { + return computeEnvironmentConfigService.getAvailable(user, project, type); + } + +} diff --git a/src/main/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApi.java b/src/main/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApi.java deleted file mode 100644 index 8c16adf..0000000 --- a/src/main/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApi.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.nrg.xnat.compute.rest; - -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import org.nrg.framework.annotations.XapiRestController; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xapi.rest.AbstractXapiRestController; -import org.nrg.xapi.rest.XapiRequestMapping; -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xnat.compute.models.ComputeSpecConfig; -import org.nrg.xnat.compute.services.ComputeSpecConfigService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -import static org.nrg.xdat.security.helpers.AccessLevel.Admin; -import static org.nrg.xdat.security.helpers.AccessLevel.Read; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; - -@Api("ComputeSpecConfigs REST API") -@XapiRestController -@RequestMapping(value = "/compute-spec-configs") -public class ComputeSpecConfigsApi extends AbstractXapiRestController { - - private final ComputeSpecConfigService computeSpecConfigService; - - @Autowired - public ComputeSpecConfigsApi(final UserManagementServiceI userManagementService, - final RoleHolder roleHolder, - final ComputeSpecConfigService computeSpecConfigService) { - super(userManagementService, roleHolder); - this.computeSpecConfigService = computeSpecConfigService; - } - - @ApiOperation(value = "Get all compute spec configs or all compute spec configs for a given type.", response = ComputeSpecConfig.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Compute spec configs successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Admin) - public List getAll(@RequestParam(value = "type", required = false) final ComputeSpecConfig.ConfigType type) { - if (type != null) { - return computeSpecConfigService.getByType(type); - } else { - return computeSpecConfigService.getAll(); - } - } - - @ApiOperation(value = "Get a compute spec config.", response = ComputeSpecConfig.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Compute spec config successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Compute spec config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Admin) - public ComputeSpecConfig get(@PathVariable("id") final Long id) throws NotFoundException { - return computeSpecConfigService.retrieve(id) - .orElseThrow(() -> new NotFoundException("Compute spec config not found.")); - } - - @ApiOperation(value = "Create a compute spec config.", response = ComputeSpecConfig.class) - @ApiResponses({ - @ApiResponse(code = 201, message = "Compute spec config successfully created."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @ResponseStatus(HttpStatus.CREATED) - @XapiRequestMapping(value = "",consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.POST, restrictTo = Admin) - public ComputeSpecConfig create(@RequestBody final ComputeSpecConfig computeSpecConfig) { - return computeSpecConfigService.create(computeSpecConfig); - } - - @ApiOperation(value = "Update a compute spec config.", response = ComputeSpecConfig.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Compute spec config successfully updated."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Compute spec config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/{id}",consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.PUT, restrictTo = Admin) - public ComputeSpecConfig update(@PathVariable("id") final Long id, - @RequestBody final ComputeSpecConfig computeSpecConfig) throws NotFoundException { - if (!id.equals(computeSpecConfig.getId())) { - throw new IllegalArgumentException("The ID in the path must match the ID in the body."); - } - - return computeSpecConfigService.update(computeSpecConfig); - } - - @ApiOperation(value = "Delete a compute spec config.") - @ApiResponses({ - @ApiResponse(code = 204, message = "Compute spec config successfully deleted."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Compute spec config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @ResponseStatus(HttpStatus.NO_CONTENT) - @XapiRequestMapping(value = "/{id}", method = RequestMethod.DELETE, restrictTo = Admin) - public void delete(@PathVariable("id") final Long id) throws NotFoundException { - computeSpecConfigService.delete(id); - } - - @ApiOperation(value = "Get all available compute spec configs for the given user and project.", response = ComputeSpecConfig.class, responseContainer = "List") - @ApiResponses({ - @ApiResponse(code = 200, message = "Compute spec configs successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/available", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Read) - public List getAvailable(@RequestParam(value = "user") final String user, - @RequestParam(value = "project") final String project, - @RequestParam(value = "type", required = false) final ComputeSpecConfig.ConfigType type) { - return computeSpecConfigService.getAvailable(user, project, type); - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigEntityService.java new file mode 100644 index 0000000..9e19830 --- /dev/null +++ b/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigEntityService.java @@ -0,0 +1,15 @@ +package org.nrg.xnat.compute.services; + +import org.nrg.framework.orm.hibernate.BaseHibernateService; +import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; +import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; + +import java.util.List; + +public interface ComputeEnvironmentConfigEntityService extends BaseHibernateService { + + void addHardwareConfigEntity(Long computeEnvironmentConfigId, Long hardwareConfigId); + void removeHardwareConfigEntity(Long computeEnvironmentConfigId, Long hardwareConfigId); + List findByType(ComputeEnvironmentConfig.ConfigType type); + +} diff --git a/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java b/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java new file mode 100644 index 0000000..6c944e9 --- /dev/null +++ b/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java @@ -0,0 +1,22 @@ +package org.nrg.xnat.compute.services; + +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; + +import java.util.List; +import java.util.Optional; + +public interface ComputeEnvironmentConfigService { + + boolean exists(Long id); + Optional retrieve(Long id); + List getAll(); + List getByType(ComputeEnvironmentConfig.ConfigType type); + List getAvailable(String user, String project); + List getAvailable(String user, String project, ComputeEnvironmentConfig.ConfigType type); + boolean isAvailable(String user, String project, Long id); + ComputeEnvironmentConfig create(ComputeEnvironmentConfig computeEnvironmentConfig); + ComputeEnvironmentConfig update(ComputeEnvironmentConfig computeEnvironmentConfig) throws NotFoundException; + void delete(Long id) throws NotFoundException; + +} diff --git a/src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigEntityService.java deleted file mode 100644 index 021985c..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigEntityService.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.nrg.xnat.compute.services; - -import org.nrg.framework.orm.hibernate.BaseHibernateService; -import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; -import org.nrg.xnat.compute.models.ComputeSpecConfig; - -import java.util.List; - -public interface ComputeSpecConfigEntityService extends BaseHibernateService { - - void addHardwareConfigEntity(Long computeSpecConfigId, Long hardwareConfigId); - void removeHardwareConfigEntity(Long computeSpecConfigId, Long hardwareConfigId); - List findByType(ComputeSpecConfig.ConfigType type); - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigService.java b/src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigService.java deleted file mode 100644 index 9c547c3..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/ComputeSpecConfigService.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.nrg.xnat.compute.services; - -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xnat.compute.models.ComputeSpecConfig; - -import java.util.List; -import java.util.Optional; - -public interface ComputeSpecConfigService { - - boolean exists(Long id); - Optional retrieve(Long id); - List getAll(); - List getByType(ComputeSpecConfig.ConfigType type); - List getAvailable(String user, String project); - List getAvailable(String user, String project, ComputeSpecConfig.ConfigType type); - boolean isAvailable(String user, String project, Long id); - ComputeSpecConfig create(ComputeSpecConfig computeSpecConfig); - ComputeSpecConfig update(ComputeSpecConfig computeSpecConfig) throws NotFoundException; - void delete(Long id) throws NotFoundException; - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java b/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java index 6155d95..bd3b089 100644 --- a/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java +++ b/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java @@ -4,7 +4,7 @@ public interface JobTemplateService { - boolean isAvailable(String user, String project, Long computeSpecConfigId, Long hardwareConfigId); - JobTemplate resolve(String user, String project, Long computeSpecConfigId, Long hardwareConfigId); + boolean isAvailable(String user, String project, Long computeEnvironmentConfigId, Long hardwareConfigId); + JobTemplate resolve(String user, String project, Long computeEnvironmentConfigId, Long hardwareConfigId); } diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java new file mode 100644 index 0000000..94de0f4 --- /dev/null +++ b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java @@ -0,0 +1,397 @@ +package org.nrg.xnat.compute.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.nrg.framework.constants.Scope; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.models.*; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class DefaultComputeEnvironmentConfigService implements ComputeEnvironmentConfigService { + + private final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService; + private final HardwareConfigEntityService hardwareConfigEntityService; + + @Autowired + public DefaultComputeEnvironmentConfigService(final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService, + final HardwareConfigEntityService hardwareConfigEntityService) { + this.computeEnvironmentConfigEntityService = computeEnvironmentConfigEntityService; + this.hardwareConfigEntityService = hardwareConfigEntityService; + } + + /** + * Checks if a ComputeEnvironmentConfig with the given ID exists. + * @param id The ID of the ComputeEnvironmentConfig to check for. + * @return True if a ComputeEnvironmentConfig with the given ID exists, false otherwise. + */ + @Override + public boolean exists(Long id) { + return computeEnvironmentConfigEntityService.exists("id", id); + } + + /** + * Gets a ComputeEnvironmentConfig by its ID. + * @param id The ID of the ComputeEnvironmentConfig to retrieve. + * @return The ComputeEnvironmentConfig with the given ID, if it exists or else an empty Optional. + */ + @Override + public Optional retrieve(Long id) { + ComputeEnvironmentConfigEntity entity = computeEnvironmentConfigEntityService.retrieve(id); + return Optional.ofNullable(entity) + .map(ComputeEnvironmentConfigEntity::toPojo); + } + + /** + * Get all ComputeEnvironmentConfigs. + * @return A list of all ComputeEnvironmentConfigs. + */ + @Override + public List getAll() { + return computeEnvironmentConfigEntityService + .getAll() + .stream() + .map(ComputeEnvironmentConfigEntity::toPojo) + .collect(Collectors.toList()); + } + + /** + * Creates a new ComputeEnvironmentConfig. + * @param type The type of the ComputeEnvironmentConfig to create. + * @return The newly created ComputeEnvironmentConfig. + */ + @Override + public List getByType(ComputeEnvironmentConfig.ConfigType type) { + return computeEnvironmentConfigEntityService + .findByType(type) + .stream() + .map(ComputeEnvironmentConfigEntity::toPojo) + .collect(Collectors.toList()); + } + + /** + * Get all ComputeEnvironmentConfigs that are available to the given user and project regardless of type. + * @param user The user to check for. + * @param project The project to check for. + * @return A list of all ComputeEnvironmentConfigs that are available to the given user and project. + */ + @Override + public List getAvailable(String user, String project) { + return getAvailable(user, project, null); + } + + /** + * Get all ComputeEnvironmentConfigs of the given type that are available to the given user and project. + * @param user The user to check for. + * @param project The project to check for. + * @param type The type of ComputeEnvironmentConfig to check for. + * @return A list of all ComputeEnvironmentConfigs of the given type that are available to the given user and project. + */ + @Override + public List getAvailable(String user, String project, ComputeEnvironmentConfig.ConfigType type) { + List all; + + if (type == null) { + all = getAll(); + } else { + all = getByType(type); + } + + final List available = all.stream().filter(computeEnvironmentConfig -> { + final ComputeEnvironmentScope siteScope = computeEnvironmentConfig.getScopes().get(Scope.Site); + final ComputeEnvironmentScope projectScope = computeEnvironmentConfig.getScopes().get(Scope.Project); + final ComputeEnvironmentScope userScope = computeEnvironmentConfig.getScopes().get(Scope.User); + + return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); + }).collect(Collectors.toList()); + + available.forEach(computeEnvironmentConfig -> { + final Set hardwareConfigs = computeEnvironmentConfig.getHardwareOptions().getHardwareConfigs(); + final Set availableHardware = hardwareConfigs.stream() + .filter(hardwareConfig -> { + final HardwareScope siteScope = hardwareConfig.getScopes().get(Scope.Site); + final HardwareScope projectScope = hardwareConfig.getScopes().get(Scope.Project); + final HardwareScope userScope = hardwareConfig.getScopes().get(Scope.User); + + return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); + }).collect(Collectors.toSet()); + + computeEnvironmentConfig.getHardwareOptions().setHardwareConfigs(availableHardware); + }); + + return available; + } + + /** + * Checks if the given ComputeEnvironmentConfig is available to the given user and project. + * @param user The user to check for. + * @param project The project to check for. + * @param id The ID of the ComputeEnvironmentConfig to check for. + * @return True if the ComputeEnvironmentConfig with the given ID is available to the given user and project, false otherwise. + */ + @Override + public boolean isAvailable(String user, String project, Long id) { + final Optional computeEnvironmentConfig = retrieve(id); + + if (!computeEnvironmentConfig.isPresent()) { + return false; + } + + final ComputeEnvironmentScope siteScope = computeEnvironmentConfig.get().getScopes().get(Scope.Site); + final ComputeEnvironmentScope projectScope = computeEnvironmentConfig.get().getScopes().get(Scope.Project); + final ComputeEnvironmentScope userScope = computeEnvironmentConfig.get().getScopes().get(Scope.User); + + return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); + } + + /** + * Creates a new ComputeEnvironmentConfig. + * @param computeEnvironmentConfig The ComputeEnvironmentConfig to create. + * @return The newly created ComputeEnvironmentConfig, with its ID set. + */ + @Override + public ComputeEnvironmentConfig create(ComputeEnvironmentConfig computeEnvironmentConfig) { + // Validate the ComputeEnvironmentConfig + validate(computeEnvironmentConfig); + + // Create the new compute environment config entity + ComputeEnvironmentConfigEntity newComputeEnvironmentConfigEntity = computeEnvironmentConfigEntityService.create(ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig)); + + // Update the pojo with the new ID, needed for association with hardware configs + computeEnvironmentConfig.setId(newComputeEnvironmentConfigEntity.getId()); + + setHardwareConfigsForComputeEnvironmentConfig(computeEnvironmentConfig); + + return computeEnvironmentConfigEntityService.retrieve(newComputeEnvironmentConfigEntity.getId()).toPojo(); + } + + /** + * Updates an existing ComputeEnvironmentConfig. + * @param computeEnvironmentConfig The ComputeEnvironmentConfig to update. + * @return The updated ComputeEnvironmentConfig. + * @throws NotFoundException If the ComputeEnvironmentConfig to update does not exist. + */ + @Override + public ComputeEnvironmentConfig update(ComputeEnvironmentConfig computeEnvironmentConfig) throws NotFoundException { + // Validate the ComputeEnvironmentConfig + if (computeEnvironmentConfig.getId() == null) { + throw new IllegalArgumentException("ComputeEnvironmentConfig ID cannot be null when updating"); + } + + validate(computeEnvironmentConfig); + + ComputeEnvironmentConfigEntity template = computeEnvironmentConfigEntityService.get(computeEnvironmentConfig.getId()); + template.update(computeEnvironmentConfig); + computeEnvironmentConfigEntityService.update(template); + + // Update the pojo with the new ID, needed for association with hardware configs + computeEnvironmentConfig.setId(template.getId()); + + setHardwareConfigsForComputeEnvironmentConfig(computeEnvironmentConfig); + + return computeEnvironmentConfigEntityService.get(computeEnvironmentConfig.getId()).toPojo(); + } + + /** + * Deletes an existing ComputeEnvironmentConfig. + * @param id The ID of the ComputeEnvironmentConfig to delete. + * @throws NotFoundException If the ComputeEnvironmentConfig to delete does not exist. + */ + @Override + public void delete(Long id) throws NotFoundException { + if (!exists(id)) { + throw new NotFoundException("No compute environment config found with ID " + id); + } + + // Remove all hardware configs from the compute environment config + // Get the ids of all hardware configs associated with the compute environment config before deleting to avoid + // a ConcurrentModificationException + Set hardwareConfigIds = computeEnvironmentConfigEntityService.retrieve(id) + .getHardwareOptions() + .getHardwareConfigs() + .stream() + .map(HardwareConfigEntity::getId) + .collect(Collectors.toSet()); + + hardwareConfigIds.forEach(hardwareConfigId -> { + computeEnvironmentConfigEntityService.removeHardwareConfigEntity(id, hardwareConfigId); + }); + + computeEnvironmentConfigEntityService.delete(id); + } + + /** + * Checks if the scope is enabled for the given user and project. + * @param user The user to check for. + * @param project The project to check for. + * @param siteScope The site scope to check. + * @param userScope The user scope to check. + * @param projectScope The project scope to check. + * @return True if the scopes are enabled for the given user and project, false otherwise. + */ + protected boolean isScopeEnabledForSiteUserAndProject(String user, String project, ComputeEnvironmentScope siteScope, ComputeEnvironmentScope userScope, ComputeEnvironmentScope projectScope) { + return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled + userScope != null && (userScope.isEnabled() || userScope.getIds().contains(user)) && // User scope must be enabled for all users or the user must be in the list + projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list + } + + /** + * Checks if the scope is enabled for the given user and project. + * @param user The user to check for. + * @param project The project to check for. + * @param siteScope The site scope to check. + * @param userScope The user scope to check. + * @param projectScope The project scope to check. + * @return True if the scopes are enabled for the given user and project, false otherwise. + */ + protected boolean isScopeEnabledForSiteUserAndProject(String user, String project, HardwareScope siteScope, HardwareScope userScope, HardwareScope projectScope) { + return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled + userScope != null && (userScope.isEnabled() || userScope.getIds().contains(user)) && // User scope must be enabled for all users or the user must be in the list + projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list + } + + /** + * Connect the hardware config entities to the compute environment config entity. + * @param computeEnvironmentConfig The compute environment config to connect the hardware configs to. + */ + protected void setHardwareConfigsForComputeEnvironmentConfig(ComputeEnvironmentConfig computeEnvironmentConfig) { + // Probably a more efficient way to do this, but it works for now + // Remove all hardware configs from the compute environment config + // Get the ids of all hardware configs associated with the compute environment config before deleting to avoid + // a ConcurrentModificationException + Set hardwareConfigIds = computeEnvironmentConfigEntityService.retrieve(computeEnvironmentConfig.getId()) + .getHardwareOptions() + .getHardwareConfigs() + .stream() + .map(HardwareConfigEntity::getId) + .collect(Collectors.toSet()); + + hardwareConfigIds.forEach(hardwareConfigId -> { + computeEnvironmentConfigEntityService.removeHardwareConfigEntity(computeEnvironmentConfig.getId(), hardwareConfigId); + }); + + // Add the hardware configs to the compute environment config + if (computeEnvironmentConfig.getHardwareOptions().isAllowAllHardware()) { + // Add all hardware configs to the compute environment config + hardwareConfigEntityService.getAll().forEach(hardwareConfigEntity -> { + computeEnvironmentConfigEntityService.addHardwareConfigEntity(computeEnvironmentConfig.getId(), hardwareConfigEntity.getId()); + }); + } else { + // Add the specified hardware configs to the compute environment config + computeEnvironmentConfig.getHardwareOptions().getHardwareConfigs().forEach(hardwareConfig -> { + if (hardwareConfig.getId() == null) { + // cant add a hardware config that doesn't exist + return; + } + + HardwareConfigEntity hardwareConfigEntity = hardwareConfigEntityService.retrieve(hardwareConfig.getId()); + + if (hardwareConfigEntity == null) { + // cant add a hardware config that doesn't exist + return; + } + + computeEnvironmentConfigEntityService.addHardwareConfigEntity(computeEnvironmentConfig.getId(), hardwareConfigEntity.getId()); + }); + } + } + + /** + * Validates the given ComputeEnvironmentConfig. Throws an IllegalArgumentException if the ComputeEnvironmentConfig is invalid. + * @param config The ComputeEnvironmentConfig to validate. + */ + protected void validate(ComputeEnvironmentConfig config) { + if (config == null) { + throw new IllegalArgumentException("ComputeEnvironmentConfig cannot be null"); + } + + List errors = new ArrayList<>(); + + errors.addAll(validate(config.getConfigTypes())); + errors.addAll(validate(config.getComputeEnvironment())); + errors.addAll(validate(config.getScopes())); + errors.addAll(validate(config.getHardwareOptions())); + + if (!errors.isEmpty()) { + throw new IllegalArgumentException("ComputeEnvironmentConfig is invalid: " + errors); + } + } + + private List validate(final Set configTypes) { + List errors = new ArrayList<>(); + + if (configTypes == null || configTypes.isEmpty()) { + errors.add("ComputeEnvironmentConfig must have at least one config type"); + } + + return errors; + } + + private List validate(final ComputeEnvironment computeEnvironment) { + List errors = new ArrayList<>(); + + if (computeEnvironment == null) { + errors.add("ComputeEnvironment cannot be null"); + return errors; + } + + if (StringUtils.isBlank(computeEnvironment.getName())) { + errors.add("ComputeEnvironment name cannot be blank"); + } + + if (StringUtils.isBlank(computeEnvironment.getImage())) { + errors.add("ComputeEnvironment image cannot be blank"); + } + + return errors; + } + + private List validate(final Map scopes) { + List errors = new ArrayList<>(); + + if (scopes == null || scopes.isEmpty()) { + errors.add("ComputeEnvironmentConfig must have at least one scope"); + return errors; + } + + if (!scopes.containsKey(Scope.Site)) { + errors.add("ComputeEnvironmentConfig must have a site scope"); + } + + if (!scopes.containsKey(Scope.User)) { + errors.add("ComputeEnvironmentConfig must have a user scope"); + } + + if (!scopes.containsKey(Scope.Project)) { + errors.add("ComputeEnvironmentConfig must have a project scope"); + } + + return errors; + } + + private List validate(final ComputeEnvironmentHardwareOptions hardwareOptions) { + List errors = new ArrayList<>(); + + if (hardwareOptions == null) { + errors.add("ComputeEnvironmentHardwareOptions cannot be null"); + return errors; + } + + if (hardwareOptions.getHardwareConfigs() == null) { + errors.add("ComputeEnvironmentHardwareOptions hardware configs cannot be null"); + } + + return errors; + } + +} diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigService.java deleted file mode 100644 index 1705aeb..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigService.java +++ /dev/null @@ -1,397 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.nrg.framework.constants.Scope; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.models.*; -import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; -import org.nrg.xnat.compute.services.ComputeSpecConfigService; -import org.nrg.xnat.compute.services.HardwareConfigEntityService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.*; -import java.util.stream.Collectors; - -@Service -@Slf4j -public class DefaultComputeSpecConfigService implements ComputeSpecConfigService { - - private final ComputeSpecConfigEntityService computeSpecConfigEntityService; - private final HardwareConfigEntityService hardwareConfigEntityService; - - @Autowired - public DefaultComputeSpecConfigService(final ComputeSpecConfigEntityService computeSpecConfigEntityService, - final HardwareConfigEntityService hardwareConfigEntityService) { - this.computeSpecConfigEntityService = computeSpecConfigEntityService; - this.hardwareConfigEntityService = hardwareConfigEntityService; - } - - /** - * Checks if a ComputeSpecConfig with the given ID exists. - * @param id The ID of the ComputeSpecConfig to check for. - * @return True if a ComputeSpecConfig with the given ID exists, false otherwise. - */ - @Override - public boolean exists(Long id) { - return computeSpecConfigEntityService.exists("id", id); - } - - /** - * Gets a ComputeSpecConfig by its ID. - * @param id The ID of the ComputeSpecConfig to retrieve. - * @return The ComputeSpecConfig with the given ID, if it exists or else an empty Optional. - */ - @Override - public Optional retrieve(Long id) { - ComputeSpecConfigEntity entity = computeSpecConfigEntityService.retrieve(id); - return Optional.ofNullable(entity) - .map(ComputeSpecConfigEntity::toPojo); - } - - /** - * Get all ComputeSpecConfigs. - * @return A list of all ComputeSpecConfigs. - */ - @Override - public List getAll() { - return computeSpecConfigEntityService - .getAll() - .stream() - .map(ComputeSpecConfigEntity::toPojo) - .collect(Collectors.toList()); - } - - /** - * Creates a new ComputeSpecConfig. - * @param type The type of the ComputeSpecConfig to create. - * @return The newly created ComputeSpecConfig. - */ - @Override - public List getByType(ComputeSpecConfig.ConfigType type) { - return computeSpecConfigEntityService - .findByType(type) - .stream() - .map(ComputeSpecConfigEntity::toPojo) - .collect(Collectors.toList()); - } - - /** - * Get all ComputeSpecConfigs that are available to the given user and project regardless of type. - * @param user The user to check for. - * @param project The project to check for. - * @return A list of all ComputeSpecConfigs that are available to the given user and project. - */ - @Override - public List getAvailable(String user, String project) { - return getAvailable(user, project, null); - } - - /** - * Get all ComputeSpecConfigs of the given type that are available to the given user and project. - * @param user The user to check for. - * @param project The project to check for. - * @param type The type of ComputeSpecConfig to check for. - * @return A list of all ComputeSpecConfigs of the given type that are available to the given user and project. - */ - @Override - public List getAvailable(String user, String project, ComputeSpecConfig.ConfigType type) { - List all; - - if (type == null) { - all = getAll(); - } else { - all = getByType(type); - } - - final List available = all.stream().filter(computeSpecConfig -> { - final ComputeSpecScope siteScope = computeSpecConfig.getScopes().get(Scope.Site); - final ComputeSpecScope projectScope = computeSpecConfig.getScopes().get(Scope.Project); - final ComputeSpecScope userScope = computeSpecConfig.getScopes().get(Scope.User); - - return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); - }).collect(Collectors.toList()); - - available.forEach(computeSpecConfig -> { - final Set hardwareConfigs = computeSpecConfig.getHardwareOptions().getHardwareConfigs(); - final Set availableHardware = hardwareConfigs.stream() - .filter(hardwareConfig -> { - final HardwareScope siteScope = hardwareConfig.getScopes().get(Scope.Site); - final HardwareScope projectScope = hardwareConfig.getScopes().get(Scope.Project); - final HardwareScope userScope = hardwareConfig.getScopes().get(Scope.User); - - return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); - }).collect(Collectors.toSet()); - - computeSpecConfig.getHardwareOptions().setHardwareConfigs(availableHardware); - }); - - return available; - } - - /** - * Checks if the given ComputeSpecConfig is available to the given user and project. - * @param user The user to check for. - * @param project The project to check for. - * @param id The ID of the ComputeSpecConfig to check for. - * @return True if the ComputeSpecConfig with the given ID is available to the given user and project, false otherwise. - */ - @Override - public boolean isAvailable(String user, String project, Long id) { - final Optional computeSpecConfig = retrieve(id); - - if (!computeSpecConfig.isPresent()) { - return false; - } - - final ComputeSpecScope siteScope = computeSpecConfig.get().getScopes().get(Scope.Site); - final ComputeSpecScope projectScope = computeSpecConfig.get().getScopes().get(Scope.Project); - final ComputeSpecScope userScope = computeSpecConfig.get().getScopes().get(Scope.User); - - return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); - } - - /** - * Creates a new ComputeSpecConfig. - * @param computeSpecConfig The ComputeSpecConfig to create. - * @return The newly created ComputeSpecConfig, with its ID set. - */ - @Override - public ComputeSpecConfig create(ComputeSpecConfig computeSpecConfig) { - // Validate the ComputeSpecConfig - validate(computeSpecConfig); - - // Create the new compute spec config entity - ComputeSpecConfigEntity newComputeSpecConfigEntity = computeSpecConfigEntityService.create(ComputeSpecConfigEntity.fromPojo(computeSpecConfig)); - - // Update the pojo with the new ID, needed for association with hardware configs - computeSpecConfig.setId(newComputeSpecConfigEntity.getId()); - - setHardwareConfigsForComputeSpecConfig(computeSpecConfig); - - return computeSpecConfigEntityService.retrieve(newComputeSpecConfigEntity.getId()).toPojo(); - } - - /** - * Updates an existing ComputeSpecConfig. - * @param computeSpecConfig The ComputeSpecConfig to update. - * @return The updated ComputeSpecConfig. - * @throws NotFoundException If the ComputeSpecConfig to update does not exist. - */ - @Override - public ComputeSpecConfig update(ComputeSpecConfig computeSpecConfig) throws NotFoundException { - // Validate the ComputeSpecConfig - if (computeSpecConfig.getId() == null) { - throw new IllegalArgumentException("ComputeSpecConfig ID cannot be null when updating"); - } - - validate(computeSpecConfig); - - ComputeSpecConfigEntity template = computeSpecConfigEntityService.get(computeSpecConfig.getId()); - template.update(computeSpecConfig); - computeSpecConfigEntityService.update(template); - - // Update the pojo with the new ID, needed for association with hardware configs - computeSpecConfig.setId(template.getId()); - - setHardwareConfigsForComputeSpecConfig(computeSpecConfig); - - return computeSpecConfigEntityService.get(computeSpecConfig.getId()).toPojo(); - } - - /** - * Deletes an existing ComputeSpecConfig. - * @param id The ID of the ComputeSpecConfig to delete. - * @throws NotFoundException If the ComputeSpecConfig to delete does not exist. - */ - @Override - public void delete(Long id) throws NotFoundException { - if (!exists(id)) { - throw new NotFoundException("No compute spec config found with ID " + id); - } - - // Remove all hardware configs from the compute spec config - // Get the ids of all hardware configs associated with the compute spec config before deleting to avoid - // a ConcurrentModificationException - Set hardwareConfigIds = computeSpecConfigEntityService.retrieve(id) - .getHardwareOptions() - .getHardwareConfigs() - .stream() - .map(HardwareConfigEntity::getId) - .collect(Collectors.toSet()); - - hardwareConfigIds.forEach(hardwareConfigId -> { - computeSpecConfigEntityService.removeHardwareConfigEntity(id, hardwareConfigId); - }); - - computeSpecConfigEntityService.delete(id); - } - - /** - * Checks if the scope is enabled for the given user and project. - * @param user The user to check for. - * @param project The project to check for. - * @param siteScope The site scope to check. - * @param userScope The user scope to check. - * @param projectScope The project scope to check. - * @return True if the scopes are enabled for the given user and project, false otherwise. - */ - protected boolean isScopeEnabledForSiteUserAndProject(String user, String project, ComputeSpecScope siteScope, ComputeSpecScope userScope, ComputeSpecScope projectScope) { - return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled - userScope != null && (userScope.isEnabled() || userScope.getIds().contains(user)) && // User scope must be enabled for all users or the user must be in the list - projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list - } - - /** - * Checks if the scope is enabled for the given user and project. - * @param user The user to check for. - * @param project The project to check for. - * @param siteScope The site scope to check. - * @param userScope The user scope to check. - * @param projectScope The project scope to check. - * @return True if the scopes are enabled for the given user and project, false otherwise. - */ - protected boolean isScopeEnabledForSiteUserAndProject(String user, String project, HardwareScope siteScope, HardwareScope userScope, HardwareScope projectScope) { - return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled - userScope != null && (userScope.isEnabled() || userScope.getIds().contains(user)) && // User scope must be enabled for all users or the user must be in the list - projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list - } - - /** - * Connect the hardware config entities to the compute spec config entity. - * @param computeSpecConfig The compute spec config to connect the hardware configs to. - */ - protected void setHardwareConfigsForComputeSpecConfig(ComputeSpecConfig computeSpecConfig) { - // Probably a more efficient way to do this, but it works for now - // Remove all hardware configs from the compute spec config - // Get the ids of all hardware configs associated with the compute spec config before deleting to avoid - // a ConcurrentModificationException - Set hardwareConfigIds = computeSpecConfigEntityService.retrieve(computeSpecConfig.getId()) - .getHardwareOptions() - .getHardwareConfigs() - .stream() - .map(HardwareConfigEntity::getId) - .collect(Collectors.toSet()); - - hardwareConfigIds.forEach(hardwareConfigId -> { - computeSpecConfigEntityService.removeHardwareConfigEntity(computeSpecConfig.getId(), hardwareConfigId); - }); - - // Add the hardware configs to the compute spec config - if (computeSpecConfig.getHardwareOptions().isAllowAllHardware()) { - // Add all hardware configs to the compute spec config - hardwareConfigEntityService.getAll().forEach(hardwareConfigEntity -> { - computeSpecConfigEntityService.addHardwareConfigEntity(computeSpecConfig.getId(), hardwareConfigEntity.getId()); - }); - } else { - // Add the specified hardware configs to the compute spec config - computeSpecConfig.getHardwareOptions().getHardwareConfigs().forEach(hardwareConfig -> { - if (hardwareConfig.getId() == null) { - // cant add a hardware config that doesn't exist - return; - } - - HardwareConfigEntity hardwareConfigEntity = hardwareConfigEntityService.retrieve(hardwareConfig.getId()); - - if (hardwareConfigEntity == null) { - // cant add a hardware config that doesn't exist - return; - } - - computeSpecConfigEntityService.addHardwareConfigEntity(computeSpecConfig.getId(), hardwareConfigEntity.getId()); - }); - } - } - - /** - * Validates the given ComputeSpecConfig. Throws an IllegalArgumentException if the ComputeSpecConfig is invalid. - * @param config The ComputeSpecConfig to validate. - */ - protected void validate(ComputeSpecConfig config) { - if (config == null) { - throw new IllegalArgumentException("ComputeSpecConfig cannot be null"); - } - - List errors = new ArrayList<>(); - - errors.addAll(validate(config.getConfigTypes())); - errors.addAll(validate(config.getComputeSpec())); - errors.addAll(validate(config.getScopes())); - errors.addAll(validate(config.getHardwareOptions())); - - if (!errors.isEmpty()) { - throw new IllegalArgumentException("ComputeSpecConfig is invalid: " + errors); - } - } - - private List validate(final Set configTypes) { - List errors = new ArrayList<>(); - - if (configTypes == null || configTypes.isEmpty()) { - errors.add("ComputeSpecConfig must have at least one config type"); - } - - return errors; - } - - private List validate(final ComputeSpec computeSpec) { - List errors = new ArrayList<>(); - - if (computeSpec == null) { - errors.add("ComputeSpec cannot be null"); - return errors; - } - - if (StringUtils.isBlank(computeSpec.getName())) { - errors.add("ComputeSpec name cannot be blank"); - } - - if (StringUtils.isBlank(computeSpec.getImage())) { - errors.add("ComputeSpec image cannot be blank"); - } - - return errors; - } - - private List validate(final Map scopes) { - List errors = new ArrayList<>(); - - if (scopes == null || scopes.isEmpty()) { - errors.add("ComputeSpecConfig must have at least one scope"); - return errors; - } - - if (!scopes.containsKey(Scope.Site)) { - errors.add("ComputeSpecConfig must have a site scope"); - } - - if (!scopes.containsKey(Scope.User)) { - errors.add("ComputeSpecConfig must have a user scope"); - } - - if (!scopes.containsKey(Scope.Project)) { - errors.add("ComputeSpecConfig must have a project scope"); - } - - return errors; - } - - private List validate(final ComputeSpecHardwareOptions hardwareOptions) { - List errors = new ArrayList<>(); - - if (hardwareOptions == null) { - errors.add("ComputeSpecHardwareOptions cannot be null"); - return errors; - } - - if (hardwareOptions.getHardwareConfigs() == null) { - errors.add("ComputeSpecHardwareOptions hardware configs cannot be null"); - } - - return errors; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java index 8b7df1e..ff8102a 100644 --- a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java @@ -4,13 +4,13 @@ import org.apache.commons.lang.StringUtils; import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; -import org.nrg.xnat.compute.entities.ComputeSpecHardwareOptionsEntity; +import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; +import org.nrg.xnat.compute.entities.ComputeEnvironmentHardwareOptionsEntity; import org.nrg.xnat.compute.entities.HardwareConfigEntity; import org.nrg.xnat.compute.models.Hardware; import org.nrg.xnat.compute.models.HardwareConfig; import org.nrg.xnat.compute.models.HardwareScope; -import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; import org.nrg.xnat.compute.services.HardwareConfigEntityService; import org.nrg.xnat.compute.services.HardwareConfigService; import org.springframework.beans.factory.annotation.Autowired; @@ -27,13 +27,13 @@ public class DefaultHardwareConfigService implements HardwareConfigService { private final HardwareConfigEntityService hardwareConfigEntityService; - private final ComputeSpecConfigEntityService computeSpecConfigEntityService; + private final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService; @Autowired public DefaultHardwareConfigService(final HardwareConfigEntityService hardwareConfigEntityService, - final ComputeSpecConfigEntityService computeSpecConfigEntityService) { + final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService) { this.hardwareConfigEntityService = hardwareConfigEntityService; - this.computeSpecConfigEntityService = computeSpecConfigEntityService; + this.computeEnvironmentConfigEntityService = computeEnvironmentConfigEntityService; } /** @@ -83,12 +83,12 @@ public HardwareConfig create(HardwareConfig hardwareConfig) { // Create the new hardware config entity HardwareConfigEntity hardwareConfigEntity = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig)); - // Add the hardware config to all compute spec configs that allow all hardware - computeSpecConfigEntityService.getAll().stream() - .map(ComputeSpecConfigEntity::getHardwareOptions) - .filter(ComputeSpecHardwareOptionsEntity::isAllowAllHardware) + // Add the hardware config to all compute environment configs that allow all hardware + computeEnvironmentConfigEntityService.getAll().stream() + .map(ComputeEnvironmentConfigEntity::getHardwareOptions) + .filter(ComputeEnvironmentHardwareOptionsEntity::isAllowAllHardware) .forEach(hardwareOptions -> { - computeSpecConfigEntityService.addHardwareConfigEntity(hardwareOptions.getId(), hardwareConfigEntity.getId()); + computeEnvironmentConfigEntityService.addHardwareConfigEntity(hardwareOptions.getId(), hardwareConfigEntity.getId()); }); return hardwareConfigEntityService.retrieve(hardwareConfigEntity.getId()).toPojo(); @@ -127,12 +127,12 @@ public void delete(Long id) throws NotFoundException { throw new NotFoundException("No hardware config found with id " + id); } - // Remove the hardware config from all compute spec configs + // Remove the hardware config from all compute environment configs // Probably a more efficient way to do this, but this is the easiest way to do it - computeSpecConfigEntityService.getAll().stream() - .map(ComputeSpecConfigEntity::getHardwareOptions) + computeEnvironmentConfigEntityService.getAll().stream() + .map(ComputeEnvironmentConfigEntity::getHardwareOptions) .forEach(hardwareOptions -> { - computeSpecConfigEntityService.removeHardwareConfigEntity(hardwareOptions.getId(), id); + computeEnvironmentConfigEntityService.removeHardwareConfigEntity(hardwareOptions.getId(), id); }); hardwareConfigEntityService.delete(id); diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java index eaabec0..c65eb56 100644 --- a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java @@ -2,7 +2,7 @@ import lombok.extern.slf4j.Slf4j; import org.nrg.xnat.compute.models.*; -import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; import org.nrg.xnat.compute.services.ConstraintConfigService; import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xnat.compute.services.JobTemplateService; @@ -16,77 +16,77 @@ @Slf4j public class DefaultJobTemplateService implements JobTemplateService { - private final ComputeSpecConfigService computeSpecConfigService; + private final ComputeEnvironmentConfigService computeEnvironmentConfigService; private final HardwareConfigService hardwareConfigService; private final ConstraintConfigService constraintConfigService; @Autowired - public DefaultJobTemplateService(final ComputeSpecConfigService computeSpecConfigService, + public DefaultJobTemplateService(final ComputeEnvironmentConfigService computeEnvironmentConfigService, final HardwareConfigService hardwareConfigService, final ConstraintConfigService constraintConfigService) { - this.computeSpecConfigService = computeSpecConfigService; + this.computeEnvironmentConfigService = computeEnvironmentConfigService; this.hardwareConfigService = hardwareConfigService; this.constraintConfigService = constraintConfigService; } /** - * Returns true if the specified compute spec config and hardware config are available for the specified user and - * project and the hardware config is allowed by the compute spec config. + * Returns true if the specified compute environment config and hardware config are available for the specified user and + * project and the hardware config is allowed by the compute environment config. * @param user the user * @param project the project - * @param computeSpecConfigId the compute spec config id + * @param computeEnvironmentConfigId the compute environment config id * @param hardwareConfigId the hardware config id - * @return true if the specified compute spec config and hardware config are available for the specified user and - * project and the hardware config is allowed by the compute spec config + * @return true if the specified compute environment config and hardware config are available for the specified user and + * project and the hardware config is allowed by the compute environment config */ @Override - public boolean isAvailable(String user, String project, Long computeSpecConfigId, Long hardwareConfigId) { - if (user == null || project == null || computeSpecConfigId == null || hardwareConfigId == null) { + public boolean isAvailable(String user, String project, Long computeEnvironmentConfigId, Long hardwareConfigId) { + if (user == null || project == null || computeEnvironmentConfigId == null || hardwareConfigId == null) { throw new IllegalArgumentException("One or more parameters is null"); } - boolean isComputeSpecConfigAvailable = computeSpecConfigService.isAvailable(user, project, computeSpecConfigId); + boolean isComputeEnvironmentConfigAvailable = computeEnvironmentConfigService.isAvailable(user, project, computeEnvironmentConfigId); boolean isHardwareConfigAvailable = hardwareConfigService.isAvailable(user, project, hardwareConfigId); - if (!isComputeSpecConfigAvailable || !isHardwareConfigAvailable) { + if (!isComputeEnvironmentConfigAvailable || !isHardwareConfigAvailable) { return false; } - ComputeSpecConfig computeSpecConfig = computeSpecConfigService - .retrieve(computeSpecConfigId) - .orElseThrow(() -> new IllegalArgumentException("ComputeSpecConfig with id " + computeSpecConfigId + " does not exist")); + ComputeEnvironmentConfig computeEnvironmentConfig = computeEnvironmentConfigService + .retrieve(computeEnvironmentConfigId) + .orElseThrow(() -> new IllegalArgumentException("ComputeEnvironmentConfig with id " + computeEnvironmentConfigId + " does not exist")); - if (computeSpecConfig.getHardwareOptions().isAllowAllHardware()) { + if (computeEnvironmentConfig.getHardwareOptions().isAllowAllHardware()) { return true; } - return computeSpecConfig.getHardwareOptions() + return computeEnvironmentConfig.getHardwareOptions() .getHardwareConfigs().stream() .map(HardwareConfig::getId) .anyMatch(id -> id.equals(hardwareConfigId)); } /** - * Returns a job template for the specified user, project, compute spec config, and hardware config. + * Returns a job template for the specified user, project, compute environment config, and hardware config. * @param user the user * @param project the project - * @param computeSpecConfigId the compute spec config id + * @param computeEnvironmentConfigId the compute environment config id * @param hardwareConfigId the hardware config id - * @return a job template complete with compute spec and hardware + * @return a job template complete with compute environment and hardware */ @Override - public JobTemplate resolve(String user, String project, Long computeSpecConfigId, Long hardwareConfigId) { - if (user == null || project == null || computeSpecConfigId == null || hardwareConfigId == null) { + public JobTemplate resolve(String user, String project, Long computeEnvironmentConfigId, Long hardwareConfigId) { + if (user == null || project == null || computeEnvironmentConfigId == null || hardwareConfigId == null) { throw new IllegalArgumentException("One or more parameters is null"); } - if (!isAvailable(user, project, computeSpecConfigId, hardwareConfigId)) { - throw new IllegalArgumentException("JobTemplate with user " + user + ", project " + project + ", computeSpecConfigId " + computeSpecConfigId + ", and hardwareConfigId " + hardwareConfigId + " is not available"); + if (!isAvailable(user, project, computeEnvironmentConfigId, hardwareConfigId)) { + throw new IllegalArgumentException("JobTemplate with user " + user + ", project " + project + ", computeEnvironmentConfigId " + computeEnvironmentConfigId + ", and hardwareConfigId " + hardwareConfigId + " is not available"); } - ComputeSpecConfig computeSpecConfig = computeSpecConfigService - .retrieve(computeSpecConfigId) - .orElseThrow(() -> new IllegalArgumentException("ComputeSpecConfig with id " + computeSpecConfigId + " does not exist")); + ComputeEnvironmentConfig computeEnvironmentConfig = computeEnvironmentConfigService + .retrieve(computeEnvironmentConfigId) + .orElseThrow(() -> new IllegalArgumentException("ComputeEnvironmentConfig with id " + computeEnvironmentConfigId + " does not exist")); HardwareConfig hardwareConfig = hardwareConfigService .retrieve(hardwareConfigId) @@ -97,7 +97,7 @@ public JobTemplate resolve(String user, String project, Long computeSpecConfigId .collect(Collectors.toList()); JobTemplate jobTemplate = JobTemplate.builder() - .computeSpec(computeSpecConfig.getComputeSpec()) + .computeEnvironment(computeEnvironmentConfig.getComputeEnvironment()) .hardware(hardwareConfig.getHardware()) .constraints(constraints) .build(); diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityService.java new file mode 100644 index 0000000..27b95ed --- /dev/null +++ b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityService.java @@ -0,0 +1,77 @@ +package org.nrg.xnat.compute.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; +import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; +import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; +import org.nrg.xnat.compute.repositories.HardwareConfigDao; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; + +@Service +@Slf4j +public class HibernateComputeEnvironmentConfigEntityService extends AbstractHibernateEntityService implements ComputeEnvironmentConfigEntityService { + + private final HardwareConfigDao hardwareConfigDao; + + // For testing + public HibernateComputeEnvironmentConfigEntityService(final ComputeEnvironmentConfigDao computeEnvironmentConfigDao, + final HardwareConfigDao hardwareConfigDao) { + super(); + this.hardwareConfigDao = hardwareConfigDao; + setDao(computeEnvironmentConfigDao); + } + + @Autowired + public HibernateComputeEnvironmentConfigEntityService(HardwareConfigDao hardwareConfigDao) { + this.hardwareConfigDao = hardwareConfigDao; + } + + /** + * Associates a hardware config with a compute environment config + * @param computeEnvironmentConfigId The compute environment config id + * @param hardwareConfigId The hardware config id to associate with the compute environment config + */ + @Override + @Transactional + public void addHardwareConfigEntity(Long computeEnvironmentConfigId, Long hardwareConfigId) { + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity = getDao().retrieve(computeEnvironmentConfigId); + HardwareConfigEntity hardwareConfigEntity = hardwareConfigDao.retrieve(hardwareConfigId); + computeEnvironmentConfigEntity.getHardwareOptions().addHardwareConfig(hardwareConfigEntity); + getDao().update(computeEnvironmentConfigEntity); + hardwareConfigDao.update(hardwareConfigEntity); + } + + /** + * Removes association between a hardware config and a compute environment config + * @param computeEnvironmentConfigId The compute environment config id + * @param hardwareConfigId The hardware config id to remove from the compute environment config + */ + @Override + @Transactional + public void removeHardwareConfigEntity(Long computeEnvironmentConfigId, Long hardwareConfigId) { + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity = getDao().retrieve(computeEnvironmentConfigId); + HardwareConfigEntity hardwareConfigEntity = hardwareConfigDao.retrieve(hardwareConfigId); + computeEnvironmentConfigEntity.getHardwareOptions().removeHardwareConfig(hardwareConfigEntity); + getDao().update(computeEnvironmentConfigEntity); + hardwareConfigDao.update(hardwareConfigEntity); + } + + /** + * Returns a list of compute environment configs by type + * @param type The type of compute environment configs to return + * @return A list of compute environment configs of the environmentified type + */ + @Override + @Transactional + public List findByType(ComputeEnvironmentConfig.ConfigType type) { + return getDao().findByType(type.name()); + } + +} diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityService.java deleted file mode 100644 index 9cd7ef6..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityService.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import lombok.extern.slf4j.Slf4j; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; -import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.models.ComputeSpecConfig; -import org.nrg.xnat.compute.repositories.ComputeSpecConfigDao; -import org.nrg.xnat.compute.repositories.HardwareConfigDao; -import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.transaction.Transactional; -import java.util.List; - -@Service -@Slf4j -public class HibernateComputeSpecConfigEntityService extends AbstractHibernateEntityService implements ComputeSpecConfigEntityService { - - private final HardwareConfigDao hardwareConfigDao; - - // For testing - public HibernateComputeSpecConfigEntityService(final ComputeSpecConfigDao computeSpecConfigDao, - final HardwareConfigDao hardwareConfigDao) { - super(); - this.hardwareConfigDao = hardwareConfigDao; - setDao(computeSpecConfigDao); - } - - @Autowired - public HibernateComputeSpecConfigEntityService(HardwareConfigDao hardwareConfigDao) { - this.hardwareConfigDao = hardwareConfigDao; - } - - /** - * Associates a hardware config with a compute spec config - * @param computeSpecConfigId The compute spec config id - * @param hardwareConfigId The hardware config id to associate with the compute spec config - */ - @Override - @Transactional - public void addHardwareConfigEntity(Long computeSpecConfigId, Long hardwareConfigId) { - ComputeSpecConfigEntity computeSpecConfigEntity = getDao().retrieve(computeSpecConfigId); - HardwareConfigEntity hardwareConfigEntity = hardwareConfigDao.retrieve(hardwareConfigId); - computeSpecConfigEntity.getHardwareOptions().addHardwareConfig(hardwareConfigEntity); - getDao().update(computeSpecConfigEntity); - hardwareConfigDao.update(hardwareConfigEntity); - } - - /** - * Removes association between a hardware config and a compute spec config - * @param computeSpecConfigId The compute spec config id - * @param hardwareConfigId The hardware config id to remove from the compute spec config - */ - @Override - @Transactional - public void removeHardwareConfigEntity(Long computeSpecConfigId, Long hardwareConfigId) { - ComputeSpecConfigEntity computeSpecConfigEntity = getDao().retrieve(computeSpecConfigId); - HardwareConfigEntity hardwareConfigEntity = hardwareConfigDao.retrieve(hardwareConfigId); - computeSpecConfigEntity.getHardwareOptions().removeHardwareConfig(hardwareConfigEntity); - getDao().update(computeSpecConfigEntity); - hardwareConfigDao.update(hardwareConfigEntity); - } - - /** - * Returns a list of compute spec configs by type - * @param type The type of compute spec configs to return - * @return A list of compute spec configs of the specified type - */ - @Override - @Transactional - public List findByType(ComputeSpecConfig.ConfigType type) { - return getDao().findByType(type.name()); - } - -} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java index 5cdd5c2..551b4f4 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializer.java @@ -3,7 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.nrg.framework.constants.Scope; import org.nrg.xnat.compute.models.*; -import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; import org.nrg.xnat.initialization.tasks.InitializingTaskException; @@ -15,7 +15,7 @@ import java.util.*; /** - * Initialization task for creating the default JupyterHub ComputeSpec and Hardware configurations. + * Initialization task for creating the default JupyterHub ComputeEnvironment and Hardware configurations. */ @Component @Slf4j @@ -23,17 +23,17 @@ public class JupyterHubEnvironmentsAndHardwareInitializer extends AbstractInitia private final XFTManagerHelper xftManagerHelper; private final XnatAppInfo appInfo; - private final ComputeSpecConfigService computeSpecConfigService; + private final ComputeEnvironmentConfigService computeEnvironmentConfigService; private final HardwareConfigService hardwareConfigService; @Autowired public JupyterHubEnvironmentsAndHardwareInitializer(final XFTManagerHelper xftManagerHelper, final XnatAppInfo appInfo, - final ComputeSpecConfigService computeSpecConfigService, + final ComputeEnvironmentConfigService computeEnvironmentConfigService, final HardwareConfigService hardwareConfigService) { this.xftManagerHelper = xftManagerHelper; this.appInfo = appInfo; - this.computeSpecConfigService = computeSpecConfigService; + this.computeEnvironmentConfigService = computeEnvironmentConfigService; this.hardwareConfigService = hardwareConfigService; } @@ -43,12 +43,12 @@ public String getTaskName() { } /** - * Builds the default ComputeSpec and Hardware configurations for JupyterHub. + * Builds the default ComputeEnvironment and Hardware configurations for JupyterHub. * @throws InitializingTaskException if the XFT or XNAT services are not initialized. */ @Override protected void callImpl() throws InitializingTaskException { - log.debug("Initializing ComputeSpecs and Hardware for JupyterHub."); + log.debug("Initializing ComputeEnvironments and Hardware for JupyterHub."); // Not sure if I need to check both of these or just one. if (!xftManagerHelper.isInitialized()) { @@ -61,8 +61,8 @@ protected void callImpl() throws InitializingTaskException { throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); } - if (computeSpecConfigService.getByType(ComputeSpecConfig.ConfigType.JUPYTERHUB).size() > 0) { - log.debug("ComputeSpecs already exist. Skipping initialization."); + if (computeEnvironmentConfigService.getByType(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB).size() > 0) { + log.debug("ComputeEnvironments already exist. Skipping initialization."); return; } @@ -71,13 +71,13 @@ protected void callImpl() throws InitializingTaskException { return; } - ComputeSpecConfig computeSpecConfig = buildDefaultComputeSpecConfig(); + ComputeEnvironmentConfig computeEnvironmentConfig = buildDefaultComputeEnvironmentConfig(); try { - computeSpecConfigService.create(computeSpecConfig); - log.info("Created default ComputeSpec for JupyterHub."); + computeEnvironmentConfigService.create(computeEnvironmentConfig); + log.info("Created default ComputeEnvironment for JupyterHub."); } catch (Exception e) { - log.error("Error creating default ComputeSpec for JupyterHub.", e); + log.error("Error creating default ComputeEnvironment for JupyterHub.", e); } HardwareConfig smallHardwareConfig = buildSmallHardwareConfig(); @@ -97,38 +97,38 @@ protected void callImpl() throws InitializingTaskException { } /** - * Builds the default ComputeSpec for JupyterHub. - * @return the default ComputeSpec for JupyterHub. + * Builds the default ComputeEnvironment for JupyterHub. + * @return the default ComputeEnvironment for JupyterHub. */ - private ComputeSpecConfig buildDefaultComputeSpecConfig() { - // Initialize the ComputeSpecConfig - ComputeSpec computeSpec = new ComputeSpec(); - Map scopes = new HashMap<>(); - ComputeSpecHardwareOptions hardwareOptions = new ComputeSpecHardwareOptions(); - ComputeSpecConfig computeSpecConfig = ComputeSpecConfig.builder() + private ComputeEnvironmentConfig buildDefaultComputeEnvironmentConfig() { + // Initialize the ComputeEnvironmentConfig + ComputeEnvironment computeEnvironment = new ComputeEnvironment(); + Map scopes = new HashMap<>(); + ComputeEnvironmentHardwareOptions hardwareOptions = new ComputeEnvironmentHardwareOptions(); + ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder() .id(null) - .configTypes(new HashSet<>(Collections.singletonList(ComputeSpecConfig.ConfigType.JUPYTERHUB))) - .computeSpec(computeSpec) + .configTypes(new HashSet<>(Collections.singletonList(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB))) + .computeEnvironment(computeEnvironment) .scopes(scopes) .hardwareOptions(hardwareOptions) .build(); - // Set the ComputeSpec values - computeSpec.setName("XNAT Datascience Notebook"); - computeSpec.setImage("xnat/datascience-notebook:latest"); - computeSpec.setEnvironmentVariables(new ArrayList<>()); - computeSpec.setMounts(new ArrayList<>()); + // Set the ComputeEnvironment values + computeEnvironment.setName("XNAT Datascience Notebook"); + computeEnvironment.setImage("xnat/datascience-notebook:latest"); + computeEnvironment.setEnvironmentVariables(new ArrayList<>()); + computeEnvironment.setMounts(new ArrayList<>()); - // Set the ComputeSpecHardwareOptions values + // Set the ComputeEnvironmentHardwareOptions values hardwareOptions.setAllowAllHardware(true); hardwareOptions.setHardwareConfigs(new HashSet<>()); - // Set the ComputeSpecScope values - scopes.put(Scope.Site, new ComputeSpecScope(Scope.Site, true, Collections.emptySet())); - scopes.put(Scope.Project, new ComputeSpecScope(Scope.Project, true, Collections.emptySet())); - scopes.put(Scope.User, new ComputeSpecScope(Scope.User, true, Collections.emptySet())); + // Set the ComputeEnvironmentScope values + scopes.put(Scope.Site, new ComputeEnvironmentScope(Scope.Site, true, Collections.emptySet())); + scopes.put(Scope.Project, new ComputeEnvironmentScope(Scope.Project, true, Collections.emptySet())); + scopes.put(Scope.User, new ComputeEnvironmentScope(Scope.User, true, Collections.emptySet())); - return computeSpecConfig; + return computeEnvironmentConfig; } /** diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java index ca1be21..7b50c73 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java @@ -22,7 +22,7 @@ public class ServerStartRequest implements UserOptions { private String projectId; private String eventTrackingId; - private Long computeSpecConfigId; + private Long computeEnvironmentConfigId; private Long hardwareConfigId; } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java index ecaa250..4780f04 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java @@ -20,6 +20,6 @@ public interface UserOptionsService { Optional retrieveUserOptions(UserI user); Optional retrieveUserOptions(UserI user, String servername); - void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, Long computeSpecConfigId, Long hardwareConfigId, String eventTrackingId); + void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, Long computeEnvironmentConfigId, Long hardwareConfigId, String eventTrackingId); } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java index 4f5f371..0e72dc4 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java @@ -152,7 +152,7 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) final String itemLabel = startRequest.getItemLabel(); final String projectId = startRequest.getProjectId(); final String eventTrackingId = startRequest.getEventTrackingId(); - final Long computeSpecConfigId = startRequest.getComputeSpecConfigId(); + final Long computeEnvironmentConfigId = startRequest.getComputeEnvironmentConfigId(); final Long hardwareConfigId = startRequest.getHardwareConfigId(); eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), xsiType, itemId, @@ -166,7 +166,7 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) return; } - if (!jobTemplateService.isAvailable(user.getUsername(), projectId, computeSpecConfigId, hardwareConfigId)) { + if (!jobTemplateService.isAvailable(user.getUsername(), projectId, computeEnvironmentConfigId, hardwareConfigId)) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, "Failed to launch Jupyter notebook server. The job template either does not exist or is not available to the user and project.")); @@ -207,7 +207,7 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) JupyterServerEventI.Operation.Start, 20, "Building notebook server container configuration.")); - userOptionsService.storeUserOptions(user, servername, xsiType, itemId, projectId, computeSpecConfigId, hardwareConfigId, eventTrackingId); + userOptionsService.storeUserOptions(user, servername, xsiType, itemId, projectId, computeEnvironmentConfigId, hardwareConfigId, eventTrackingId); eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, 30, @@ -323,12 +323,12 @@ public void validateServerStartRequest(UserI user, ServerStartRequest request) { errorMessages.add("Event tracking ID cannot be blank"); } - if (request.getComputeSpecConfigId() == null) { - errorMessages.add("Compute spec config ID cannot be null"); + if (request.getComputeEnvironmentConfigId() == null) { + errorMessages.add("Compute environment config ID cannot be null"); } if (request.getHardwareConfigId() == null) { - errorMessages.add("Compute resource cannot be null"); + errorMessages.add("hardware config id cannot be null"); } if (!errorMessages.isEmpty()) { diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java index ecdb529..d0ad733 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java @@ -273,7 +273,7 @@ public Optional retrieveUserOptions(UserI user, String serverna @Override public void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, - Long computeSpecConfigId, Long hardwareConfigId, String eventTrackingId) { + Long computeEnvironmentConfigId, Long hardwareConfigId, String eventTrackingId) { log.debug("Storing user options for user '{}' server '{}' xsiType '{}' id '{}' projectId '{}'", user.getUsername(), servername, xsiType, id, projectId); @@ -282,7 +282,7 @@ public void storeUserOptions(UserI user, String servername, String xsiType, Stri } // Resolve the job template before resolving the paths - JobTemplate jobTemplate = jobTemplateService.resolve(user.getUsername(), projectId, computeSpecConfigId, hardwareConfigId); + JobTemplate jobTemplate = jobTemplateService.resolve(user.getUsername(), projectId, computeEnvironmentConfigId, hardwareConfigId); // specific xsi type -> general xsi type if (instanceOf(xsiType, XnatExperimentdata.SCHEMA_ELEMENT_NAME)) { @@ -450,22 +450,22 @@ protected TaskTemplate toTaskTemplate(@NonNull JobTemplate jobTemplate) { .placement(placement) .build(); - // Get the compute spec, hardware, and constraints from the job template - ComputeSpec computeSpec = jobTemplate.getComputeSpec(); + // Get the compute environment, hardware, and constraints from the job template + ComputeEnvironment computeEnvironment = jobTemplate.getComputeEnvironment(); Hardware hardware = jobTemplate.getHardware(); List constraints = jobTemplate.getConstraints(); // Build container spec for the task template - containerSpec.setImage(computeSpec.getImage()); + containerSpec.setImage(computeEnvironment.getImage()); - // Add environment variables from compute spec and hardware + // Add environment variables from compute environment and hardware Map environmentVariables = new HashMap<>(); - Map computeSpecEnvVars = computeSpec.getEnvironmentVariables().stream().collect(Collectors.toMap(EnvironmentVariable::getKey, EnvironmentVariable::getValue)); + Map computeEnvironmentEnvVars = computeEnvironment.getEnvironmentVariables().stream().collect(Collectors.toMap(EnvironmentVariable::getKey, EnvironmentVariable::getValue)); Map hardwareEnvVars = hardware.getEnvironmentVariables().stream().collect(Collectors.toMap(EnvironmentVariable::getKey, EnvironmentVariable::getValue)); // check for empty otherwise putAll will throw an exception - if (!computeSpecEnvVars.isEmpty()) { - environmentVariables.putAll(computeSpecEnvVars); + if (!computeEnvironmentEnvVars.isEmpty()) { + environmentVariables.putAll(computeEnvironmentEnvVars); } // check for empty otherwise putAll will throw an exception @@ -475,11 +475,11 @@ protected TaskTemplate toTaskTemplate(@NonNull JobTemplate jobTemplate) { containerSpec.setEnv(environmentVariables); - // Add mounts from compute spec and hardware + // Add mounts from compute environment and hardware // check for empty otherwise addAll will throw an exception List mounts = new ArrayList<>(); - if (!computeSpec.getMounts().isEmpty()) { - mounts.addAll(computeSpec.getMounts().stream().map(mount -> { + if (!computeEnvironment.getMounts().isEmpty()) { + mounts.addAll(computeEnvironment.getMounts().stream().map(mount -> { return Mount.builder() .source(mount.getLocalPath()) .target(mount.getContainerPath()) diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-spec-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-environment-configs.js similarity index 88% rename from src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-spec-configs.js rename to src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-environment-configs.js index 10caadb..01f0157 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-spec-configs.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-environment-configs.js @@ -1,8 +1,8 @@ -console.debug("Loading compute-spec-configs.js"); +console.debug("Loading compute-environment-configs.js"); var XNAT = getObject(XNAT || {}); XNAT.compute = getObject(XNAT.compute|| {}); -XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || {}); +XNAT.compute.computeEnvironmentConfigs = getObject(XNAT.compute.computeEnvironmentConfigs || {}); (function (factory) { if (typeof define === 'function' && define.amd) { @@ -14,9 +14,9 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { } }(function () { - XNAT.compute.computeSpecConfigs.get = async (id) => { - console.debug("Fetching compute spec config " + id) - const url = XNAT.url.csrfUrl(`/xapi/compute-spec-configs/${id}`); + XNAT.compute.computeEnvironmentConfigs.get = async (id) => { + console.debug("Fetching compute environment config " + id) + const url = XNAT.url.csrfUrl(`/xapi/compute-environment-configs/${id}`); const response = await fetch(url, { method: 'GET', @@ -28,16 +28,16 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { if (response.ok) { return response.json(); } else { - throw new Error(`Error fetching compute spec config ${id}`); + throw new Error(`Error fetching compute environment config ${id}`); } } - XNAT.compute.computeSpecConfigs.getAll = async (type) => { - console.debug("Fetching compute spec configs") + XNAT.compute.computeEnvironmentConfigs.getAll = async (type) => { + console.debug("Fetching compute environment configs") const url = type ? - XNAT.url.csrfUrl(`/xapi/compute-spec-configs?type=${type}`) : - XNAT.url.csrfUrl(`/xapi/compute-spec-configs`); + XNAT.url.csrfUrl(`/xapi/compute-environment-configs?type=${type}`) : + XNAT.url.csrfUrl(`/xapi/compute-environment-configs`); const response = await fetch(url, { method: 'GET', @@ -49,83 +49,83 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { if (response.ok) { return response.json(); } else { - throw new Error(`Error fetching compute spec configs`); + throw new Error(`Error fetching compute environment configs`); } } - XNAT.compute.computeSpecConfigs.create = async (computeSpec) => { - console.debug("Creating compute spec config") - const url = XNAT.url.csrfUrl(`/xapi/compute-spec-configs`); + XNAT.compute.computeEnvironmentConfigs.create = async (computeEnvironment) => { + console.debug("Creating compute environment config") + const url = XNAT.url.csrfUrl(`/xapi/compute-environment-configs`); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(computeSpec) + body: JSON.stringify(computeEnvironment) }); if (response.ok) { return response.json(); } else { - throw new Error(`Error creating compute spec config`); + throw new Error(`Error creating compute environment config`); } } - XNAT.compute.computeSpecConfigs.update = async (computeSpec) => { - console.debug("Updating compute spec config") - const id = computeSpec['id']; + XNAT.compute.computeEnvironmentConfigs.update = async (computeEnvironment) => { + console.debug("Updating compute environment config") + const id = computeEnvironment['id']; if (!id) { - throw new Error(`Cannot update compute spec config without an ID`); + throw new Error(`Cannot update compute environment config without an ID`); } - const url = XNAT.url.csrfUrl(`/xapi/compute-spec-configs/${id}`); + const url = XNAT.url.csrfUrl(`/xapi/compute-environment-configs/${id}`); const response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(computeSpec) + body: JSON.stringify(computeEnvironment) }); if (response.ok) { return response.json(); } else { - throw new Error(`Error updating compute spec config ${id}`); + throw new Error(`Error updating compute environment config ${id}`); } } - XNAT.compute.computeSpecConfigs.save = async (computeSpec) => { - console.debug("Saving compute spec config") - const id = computeSpec['id']; + XNAT.compute.computeEnvironmentConfigs.save = async (computeEnvironment) => { + console.debug("Saving compute environment config") + const id = computeEnvironment['id']; if (id) { - return XNAT.compute.computeSpecConfigs.update(computeSpec); + return XNAT.compute.computeEnvironmentConfigs.update(computeEnvironment); } else { - return XNAT.compute.computeSpecConfigs.create(computeSpec); + return XNAT.compute.computeEnvironmentConfigs.create(computeEnvironment); } } - XNAT.compute.computeSpecConfigs.delete = async (id) => { - console.debug("Deleting compute spec config " + id) - const url = XNAT.url.csrfUrl(`/xapi/compute-spec-configs/${id}`); + XNAT.compute.computeEnvironmentConfigs.delete = async (id) => { + console.debug("Deleting compute environment config " + id) + const url = XNAT.url.csrfUrl(`/xapi/compute-environment-configs/${id}`); const response = await fetch(url, { method: 'DELETE', }); if (!response.ok) { - throw new Error(`Error deleting compute spec config ${id}`); + throw new Error(`Error deleting compute environment config ${id}`); } } - XNAT.compute.computeSpecConfigs.available = async (type, user, project) => { - console.debug(`Fetching available compute spec configs for type ${type} and user ${user} and project ${project}`); + XNAT.compute.computeEnvironmentConfigs.available = async (type, user, project) => { + console.debug(`Fetching available compute environment configs for type ${type} and user ${user} and project ${project}`); const url = type ? - XNAT.url.csrfUrl(`/xapi/compute-spec-configs/available?type=${type}&user=${user}&project=${project}`) : - XNAT.url.csrfUrl(`/xapi/compute-spec-configs/available?user=${user}&project=${project}`); + XNAT.url.csrfUrl(`/xapi/compute-environment-configs/available?type=${type}&user=${user}&project=${project}`) : + XNAT.url.csrfUrl(`/xapi/compute-environment-configs/available?user=${user}&project=${project}`); const response = await fetch(url, { method: 'GET', @@ -137,21 +137,21 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { if (response.ok) { return response.json(); } else { - throw new Error(`Error fetching available compute spec configs`); + throw new Error(`Error fetching available compute environment configs`); } } - XNAT.compute.computeSpecConfigs.manager = async (containerId, computeSpecType) => { - console.debug("Initializing compute spec manager") + XNAT.compute.computeEnvironmentConfigs.manager = async (containerId, computeEnvironmentType) => { + console.debug("Initializing compute environment manager") let container, footer, - computeSpecConfigs = [], + computeEnvironmentConfigs = [], hardwareConfigs = [], users = [], projects = []; - computeSpecType = computeSpecType || ''; + computeEnvironmentType = computeEnvironmentType || ''; const init = () => { container = document.getElementById(containerId); @@ -206,7 +206,7 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { let button = spawn('div', [ spawn('div.pull-right', [ - spawn('button.btn.btn-sm.submit', { html: 'New ' + (computeSpecType === 'JUPYTERHUB' ? 'Jupyter Environment' : 'ComputeSpec'), onclick: () => editor(null, 'new') }) + spawn('button.btn.btn-sm.submit', { html: 'New ' + (computeEnvironmentType === 'JUPYTERHUB' ? 'Jupyter Environment' : 'computeEnvironment'), onclick: () => editor(null, 'new') }) ]), spawn('div.clear.clearFix') ]) @@ -225,11 +225,11 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { let enabled = ckbox.checked; config['scopes']['Site']['enabled'] = enabled; - XNAT.compute.computeSpecConfigs.update(config).then(() => { - XNAT.ui.banner.top(2000, `Compute Spec ${enabled ? 'Enabled' : 'Disabled'}`, 'success'); + XNAT.compute.computeEnvironmentConfigs.update(config).then(() => { + XNAT.ui.banner.top(2000, `Compute Environment ${enabled ? 'Enabled' : 'Disabled'}`, 'success'); }).catch((err) => { console.error(err); - XNAT.ui.banner.top(4000, `Error ${enabled ? 'Enabling' : 'Disabling'} Compute Spec`, 'error'); + XNAT.ui.banner.top(4000, `Error ${enabled ? 'Enabling' : 'Disabling'} Compute Environment`, 'error'); toggleCheckbox(!enabled); }); } @@ -354,36 +354,36 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { } /** - * Open the editor for a compute spec config - * @param config - the compute spec config to edit + * Open the editor for a compute environment config + * @param config - the compute environment config to edit * @param action - the action to perform (new, edit, or copy) */ const editor = (config, action) => { - console.debug("Opening compute spec config editor") + console.debug("Opening compute environment config editor") let isNew = action === 'new', isCopy = action === 'copy', isEdit = action === 'edit', title = isNew || isCopy ? 'New ' : 'Edit ', isJupyter = config ? config.configTypes.includes('JUPYTERHUB') : false; - title = title + (isJupyter ? 'Jupyter Environment' : 'ComputeSpec'); + title = title + (isJupyter ? 'Jupyter Environment' : 'computeEnvironment'); XNAT.dialog.open({ title: title, - content: spawn('div', { id: 'compute-spec-editor' }), + content: spawn('div', { id: 'compute-environment-editor' }), width: 650, maxBtn: true, beforeShow: () => { - const formEl = document.getElementById('compute-spec-editor'); + const formEl = document.getElementById('compute-environment-editor'); formEl.classList.add('panel'); let id = isNew || isCopy ? '' : config.id; - let type = computeSpecType; - let name = isNew || isCopy ? '' : config.computeSpec?.name || ''; - let image = isNew ? '' : config.computeSpec?.image; - let environmentVariables = isNew ? [] : config.computeSpec?.environmentVariables; - let mounts = isNew ? [] : config.computeSpec?.mounts; + let type = computeEnvironmentType; + let name = isNew || isCopy ? '' : config.computeEnvironment?.name || ''; + let image = isNew ? '' : config.computeEnvironment?.image; + let environmentVariables = isNew ? [] : config.computeEnvironment?.environmentVariables; + let mounts = isNew ? [] : config.computeEnvironment?.mounts; let siteEnabled = isNew ? true : config.scopes?.Site?.enabled; let allUsersEnabled = isNew ? true : config.scopes?.User?.enabled; @@ -466,7 +466,7 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { name: 'siteEnabled', id: 'siteEnabled', label: 'Site Enabled', - description: 'Enable this ComputeSpec site-wide.', + description: 'Enable this ComputeEnvironment site-wide.', value: siteEnabled, }); @@ -625,7 +625,7 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { isDefault: true, close: false, action: function() { - const formEl = document.getElementById('compute-spec-editor'); + const formEl = document.getElementById('compute-environment-editor'); const idEl = formEl.querySelector('#id'); const typeEl = formEl.querySelector('#type'); @@ -692,7 +692,7 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { config = { id: idEl.value, configTypes: [typeEl.value], - computeSpec: { + computeEnvironment: { name: nameEl.value, image: imageEl.value, environmentVariables: getEnvironmentVariables(), @@ -724,7 +724,7 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { } } - XNAT.compute.computeSpecConfigs.save(config) + XNAT.compute.computeEnvironmentConfigs.save(config) .then(() => { refreshTable(); XNAT.dialog.closeAll(); @@ -741,11 +741,11 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { } const refreshTable = () => { - XNAT.compute.computeSpecConfigs.getAll(computeSpecType) + XNAT.compute.computeEnvironmentConfigs.getAll(computeEnvironmentType) .then((data) => { - computeSpecConfigs = data; + computeEnvironmentConfigs = data; - if (computeSpecConfigs.length === 0) { + if (computeEnvironmentConfigs.length === 0) { clearContainer() container.innerHTML = `

    No Jupyter environments found

    ` return; @@ -761,7 +761,7 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { } const deleteConfig = (id) => { - XNAT.compute.computeSpecConfigs.delete(id) + XNAT.compute.computeEnvironmentConfigs.delete(id) .then(() => { XNAT.ui.banner.top(2000, 'Delete successful', 'success'); refreshTable(); @@ -773,7 +773,7 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { } const addEnvironmentVariable = (envVar) => { - const formEl = document.getElementById('compute-spec-editor'); + const formEl = document.getElementById('compute-environment-editor'); const environmentVariablesEl = formEl.querySelector('div.environment-variables'); const keyEl = spawn('input.form-control.key', { @@ -834,7 +834,7 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { } const getEnvironmentVariables = () => { - const formEl = document.getElementById('compute-spec-editor'); + const formEl = document.getElementById('compute-environment-editor'); const environmentVariablesEl = formEl.querySelector('div.environment-variables'); let environmentVariables = []; @@ -855,7 +855,7 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { } const addMount = (mount) => { - const formEl = document.getElementById('compute-spec-editor'); + const formEl = document.getElementById('compute-environment-editor'); const mountsEl = formEl.querySelector('div.mounts'); let removeButton = spawn('a.close', { @@ -907,7 +907,7 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { } const getMounts = () => { - const formEl = document.getElementById('compute-spec-editor'); + const formEl = document.getElementById('compute-environment-editor'); const mountsEl = formEl.querySelector('div.mounts'); let mounts = []; @@ -931,7 +931,7 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { } const table = () => { - const computeSpecTable = XNAT.table.dataTable(computeSpecConfigs, { + const computeEnvironmentTable = XNAT.table.dataTable(computeEnvironmentConfigs, { header: true, sortable: 'name', columns: { @@ -940,14 +940,14 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { filter: true, td: { className: 'word-wrapped align-top' }, apply: function () { - return this['computeSpec']['name']; + return this['computeEnvironment']['name']; } }, image: { label: 'Image', filter: true, apply: function () { - return this['computeSpec']['image']; + return this['computeEnvironment']['image']; }, }, hardware: { @@ -997,14 +997,14 @@ XNAT.compute.computeSpecConfigs = getObject(XNAT.compute.computeSpecConfigs || { }) clearContainer(); - computeSpecTable.render(`#${containerId}`); + computeEnvironmentTable.render(`#${containerId}`); } init(); return { container: container, - computeSpecConfigs: computeSpecConfigs, + computeEnvironmentConfigs: computeEnvironmentConfigs, refresh: refreshTable }; } diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js index 47eb524..00c3dee 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js @@ -176,7 +176,7 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s console.debug(`jupyterhub-servers.js: XNAT.plugin.jupyterhub.servers.startServer`); console.debug(`Launching jupyter server. User: ${username}, Server Name: ${servername}, XSI Type: ${xsiType}, ID: ${itemId}, Label: ${itemLabel}, Project ID: ${projectId}, eventTrackingId: ${eventTrackingId}`); - XNAT.compute.computeSpecConfigs.available("JUPYTERHUB", username, projectId).then(computeSpecConfigs => { + XNAT.compute.computeEnvironmentConfigs.available("JUPYTERHUB", username, projectId).then(computeEnvironmentConfigs => { const cancelButton = { label: 'Cancel', isDefault: false, @@ -188,10 +188,10 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s isDefault: true, close: false, action: function(obj) { - const computeSpecConfigId = document.querySelector('select#compute-spec-config').value; + const computeEnvironmentConfigId = document.querySelector('select#compute-environment-config').value; const hardwareConfigId = document.querySelector('select#hardware-config').value; - if (!computeSpecConfigId) { + if (!computeEnvironmentConfigId) { XNAT.dialog.open({ width: 450, title: "Error", @@ -233,7 +233,7 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s 'itemLabel': itemLabel, 'projectId': projectId, 'eventTrackingId': eventTrackingId, - 'computeSpecConfigId': computeSpecConfigId, + 'computeEnvironmentConfigId': computeEnvironmentConfigId, 'hardwareConfigId': hardwareConfigId, } @@ -259,7 +259,7 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s } } - const buttons = (computeSpecConfigs.length === 0) ? [cancelButton] : [cancelButton, startButton]; + const buttons = (computeEnvironmentConfigs.length === 0) ? [cancelButton] : [cancelButton, startButton]; XNAT.dialog.open({ title: 'Start Jupyter', @@ -270,8 +270,8 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s const form = document.getElementById('server-start-request-form'); form.classList.add('panel'); - if (computeSpecConfigs.length === 0) { - obj.$modal.find('.xnat-dialog-content').html('
    No compute specs are available for Jupyter. Please contact your administrator.
    '); + if (computeEnvironmentConfigs.length === 0) { + obj.$modal.find('.xnat-dialog-content').html('
    No compute environments are available for Jupyter. Please contact your administrator.
    '); return; } @@ -310,12 +310,12 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s xnatData.querySelector('p.experiment').remove(); } - let computeSpecConfigSelect = spawn('div', { style : { marginTop: '20px', marginBottom: '40px', } }, [ + let computeEnvironmentConfigSelect = spawn('div', { style : { marginTop: '20px', marginBottom: '40px', } }, [ spawn('h2', 'Jupyter Environment'), spawn('p.description', 'Select from the list of Jupyter environments available to you. This determines the software available to your Jupyter notebook server.'), - spawn('select#compute-spec-config', [ + spawn('select#compute-environment-config', [ spawn('option', {value: ''}, 'Select a Jupyter environment'), - ...computeSpecConfigs.map(c => spawn('option', {value: c['id']}, c['computeSpec']['name'])), + ...computeEnvironmentConfigs.map(c => spawn('option', {value: c['id']}, c['computeEnvironment']['name'])), ])] ); @@ -329,16 +329,16 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s form.appendChild(spawn('!', [ xnatData, - computeSpecConfigSelect, + computeEnvironmentConfigSelect, hardwareConfigSelect ])); - computeSpecConfigSelect.querySelector('select').addEventListener('change', () => { + computeEnvironmentConfigSelect.querySelector('select').addEventListener('change', () => { let hardwareSelect = document.getElementById('hardware-config'); hardwareSelect.innerHTML = ''; hardwareSelect.appendChild(spawn('option', {value: ''}, 'Select Hardware')); - let computeSpecConfig = computeSpecConfigs.filter(c => c['id'].toString() === computeSpecConfigSelect.querySelector('select').value)[0]; - let hardwareConfigs = computeSpecConfig['hardwareOptions']['hardwareConfigs']; + let computeEnvironmentConfig = computeEnvironmentConfigs.filter(c => c['id'].toString() === computeEnvironmentConfigSelect.querySelector('select').value)[0]; + let hardwareConfigs = computeEnvironmentConfig['hardwareOptions']['hardwareConfigs']; hardwareConfigs.forEach(h => { hardwareSelect.appendChild(spawn('option', {value: h['id']}, h['hardware']['name'])); }); diff --git a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm index d8d4076..43defcb 100644 --- a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm +++ b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm @@ -12,5 +12,5 @@ - + \ No newline at end of file diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index ff06aaf..82dff63 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -176,16 +176,16 @@ jupyterEnvironments: are associated with a specific Docker image and additional configuration options (such as environment variables and mounts) can be specified. An environment can also be associated with a Hardware configuration that defines the hardware resources that will be allocated to the single-user server when it is launched." - computeSpecsTable: - tag: "div#jupyterhub-compute-spec-configs-table" - computeSpecsScript: - tag: script|src="~/scripts/xnat/plugin/compute/compute-spec-configs.js" + computeEnvironmentConfigsTable: + tag: "div#jupyterhub-compute-environment-configs-table" + computeEnvironmentConfigsScript: + tag: script|src="~/scripts/xnat/plugin/compute/compute-environment-configs.js" hardwareConfigsScript: tag: script|src="~/scripts/xnat/plugin/compute/hardware-configs.js" - renderComputeSpecsTable: + renderComputeEnvironmentConfigsTable: tag: script content: > - XNAT.compute.computeSpecConfigs.manager('jupyterhub-compute-spec-configs-table', 'JUPYTERHUB'); + XNAT.compute.computeEnvironmentConfigs.manager('jupyterhub-compute-environment-configs-table', 'JUPYTERHUB'); hardwareConfigs: kind: panel diff --git a/src/test/java/org/nrg/xnat/compute/config/ComputeSpecConfigsApiConfig.java b/src/test/java/org/nrg/xnat/compute/config/ComputeEnvironmentConfigsApiConfig.java similarity index 65% rename from src/test/java/org/nrg/xnat/compute/config/ComputeSpecConfigsApiConfig.java rename to src/test/java/org/nrg/xnat/compute/config/ComputeEnvironmentConfigsApiConfig.java index 078989b..bd20e1c 100644 --- a/src/test/java/org/nrg/xnat/compute/config/ComputeSpecConfigsApiConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/ComputeEnvironmentConfigsApiConfig.java @@ -1,8 +1,8 @@ package org.nrg.xnat.compute.config; import org.nrg.framework.services.ContextService; -import org.nrg.xnat.compute.rest.ComputeSpecConfigsApi; -import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.rest.ComputeEnvironmentConfigsApi; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; import org.springframework.context.ApplicationContext; @@ -19,15 +19,17 @@ @EnableWebMvc @EnableWebSecurity @Import({MockConfig.class, RestApiTestConfig.class}) -public class ComputeSpecConfigsApiConfig extends WebSecurityConfigurerAdapter { +public class ComputeEnvironmentConfigsApiConfig extends WebSecurityConfigurerAdapter { @Bean - public ComputeSpecConfigsApi computeSpecConfigsApi(final UserManagementServiceI mockUserManagementService, - final RoleHolder mockRoleHolder, - final ComputeSpecConfigService mockComputeSpecConfigService) { - return new ComputeSpecConfigsApi(mockUserManagementService, - mockRoleHolder, - mockComputeSpecConfigService); + public ComputeEnvironmentConfigsApi computeEnvironmentConfigsApi(final UserManagementServiceI mockUserManagementService, + final RoleHolder mockRoleHolder, + final ComputeEnvironmentConfigService mockComputeEnvironmentConfigService) { + return new ComputeEnvironmentConfigsApi( + mockUserManagementService, + mockRoleHolder, + mockComputeEnvironmentConfigService + ); } @Bean diff --git a/src/test/java/org/nrg/xnat/compute/config/DefaultComputeEnvironmentConfigServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultComputeEnvironmentConfigServiceTestConfig.java new file mode 100644 index 0000000..5064e54 --- /dev/null +++ b/src/test/java/org/nrg/xnat/compute/config/DefaultComputeEnvironmentConfigServiceTestConfig.java @@ -0,0 +1,23 @@ +package org.nrg.xnat.compute.config; + +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.services.impl.DefaultComputeEnvironmentConfigService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({HibernateEntityServicesConfig.class}) +public class DefaultComputeEnvironmentConfigServiceTestConfig { + + @Bean + public DefaultComputeEnvironmentConfigService defaultComputeEnvironmentConfigService(final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService, + final HardwareConfigEntityService hardwareConfigEntityService) { + return new DefaultComputeEnvironmentConfigService( + computeEnvironmentConfigEntityService, + hardwareConfigEntityService + ); + } + +} diff --git a/src/test/java/org/nrg/xnat/compute/config/DefaultComputeSpecConfigServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultComputeSpecConfigServiceTestConfig.java deleted file mode 100644 index b6d197c..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/DefaultComputeSpecConfigServiceTestConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; -import org.nrg.xnat.compute.services.HardwareConfigEntityService; -import org.nrg.xnat.compute.services.impl.DefaultComputeSpecConfigService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import({HibernateEntityServicesConfig.class}) -public class DefaultComputeSpecConfigServiceTestConfig { - - @Bean - public DefaultComputeSpecConfigService defaultComputeSpecConfigService(final ComputeSpecConfigEntityService computeSpecConfigEntityService, - final HardwareConfigEntityService hardwareConfigEntityService) { - return new DefaultComputeSpecConfigService( - computeSpecConfigEntityService, - hardwareConfigEntityService - ); - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/DefaultHardwareConfigServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultHardwareConfigServiceTestConfig.java index a98126f..a53ef0d 100644 --- a/src/test/java/org/nrg/xnat/compute/config/DefaultHardwareConfigServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/DefaultHardwareConfigServiceTestConfig.java @@ -1,6 +1,6 @@ package org.nrg.xnat.compute.config; -import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; import org.nrg.xnat.compute.services.HardwareConfigEntityService; import org.nrg.xnat.compute.services.impl.DefaultHardwareConfigService; import org.springframework.context.annotation.Bean; @@ -13,10 +13,10 @@ public class DefaultHardwareConfigServiceTestConfig { @Bean public DefaultHardwareConfigService defaultHardwareConfigService(final HardwareConfigEntityService hardwareConfigEntityService, - final ComputeSpecConfigEntityService computeSpecConfigEntityService) { + final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService) { return new DefaultHardwareConfigService( hardwareConfigEntityService, - computeSpecConfigEntityService + computeEnvironmentConfigEntityService ); } diff --git a/src/test/java/org/nrg/xnat/compute/config/DefaultJobTemplateServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultJobTemplateServiceTestConfig.java index 52fd166..190e3be 100644 --- a/src/test/java/org/nrg/xnat/compute/config/DefaultJobTemplateServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/DefaultJobTemplateServiceTestConfig.java @@ -1,6 +1,6 @@ package org.nrg.xnat.compute.config; -import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; import org.nrg.xnat.compute.services.ConstraintConfigService; import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xnat.compute.services.impl.DefaultJobTemplateService; @@ -13,11 +13,11 @@ public class DefaultJobTemplateServiceTestConfig { @Bean - public DefaultJobTemplateService defaultJobTemplateService(final ComputeSpecConfigService mockComputeSpecConfigService, + public DefaultJobTemplateService defaultJobTemplateService(final ComputeEnvironmentConfigService mockComputeEnvironmentConfigService, final HardwareConfigService mockHardwareConfigService, final ConstraintConfigService mockConstraintConfigService) { return new DefaultJobTemplateService( - mockComputeSpecConfigService, + mockComputeEnvironmentConfigService, mockHardwareConfigService, mockConstraintConfigService ); diff --git a/src/test/java/org/nrg/xnat/compute/config/HibernateComputeSpecConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateComputeEnvironmentConfigEntityServiceTestConfig.java similarity index 63% rename from src/test/java/org/nrg/xnat/compute/config/HibernateComputeSpecConfigEntityServiceTestConfig.java rename to src/test/java/org/nrg/xnat/compute/config/HibernateComputeEnvironmentConfigEntityServiceTestConfig.java index 9f26146..d8c46dc 100644 --- a/src/test/java/org/nrg/xnat/compute/config/HibernateComputeSpecConfigEntityServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/HibernateComputeEnvironmentConfigEntityServiceTestConfig.java @@ -2,10 +2,10 @@ import org.hibernate.SessionFactory; import org.nrg.xnat.compute.entities.*; -import org.nrg.xnat.compute.repositories.ComputeSpecConfigDao; +import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; import org.nrg.xnat.compute.repositories.ConstraintConfigDao; import org.nrg.xnat.compute.repositories.HardwareConfigDao; -import org.nrg.xnat.compute.services.impl.HibernateComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.impl.HibernateComputeEnvironmentConfigEntityService; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -19,13 +19,13 @@ @Configuration @Import({HibernateConfig.class}) -public class HibernateComputeSpecConfigEntityServiceTestConfig { +public class HibernateComputeEnvironmentConfigEntityServiceTestConfig { @Bean - public HibernateComputeSpecConfigEntityService hibernateComputeSpecConfigEntityServiceTest(@Qualifier("computeSpecConfigDaoImpl") final ComputeSpecConfigDao computeSpecConfigDaoImpl, - @Qualifier("hardwareConfigDaoImpl") final HardwareConfigDao hardwareConfigDaoImpl) { - return new HibernateComputeSpecConfigEntityService( - computeSpecConfigDaoImpl, + public HibernateComputeEnvironmentConfigEntityService hibernateComputeEnvironmentConfigEntityServiceTest(@Qualifier("computeEnvironmentConfigDaoImpl") final ComputeEnvironmentConfigDao computeEnvironmentConfigDaoImpl, + @Qualifier("hardwareConfigDaoImpl") final HardwareConfigDao hardwareConfigDaoImpl) { + return new HibernateComputeEnvironmentConfigEntityService( + computeEnvironmentConfigDaoImpl, hardwareConfigDaoImpl); } @@ -38,10 +38,10 @@ public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qual ConstraintConfigEntity.class, ConstraintEntity.class, ConstraintScopeEntity.class, - ComputeSpecConfigEntity.class, - ComputeSpecEntity.class, - ComputeSpecScopeEntity.class, - ComputeSpecHardwareOptionsEntity.class, + ComputeEnvironmentConfigEntity.class, + ComputeEnvironmentEntity.class, + ComputeEnvironmentScopeEntity.class, + ComputeEnvironmentHardwareOptionsEntity.class, HardwareConfigEntity.class, HardwareEntity.class, HardwareScopeEntity.class, @@ -70,10 +70,10 @@ public HardwareConfigDao hardwareConfigDaoImpl(final SessionFactory sessionFacto } @Bean - @Qualifier("computeSpecConfigDaoImpl") - public ComputeSpecConfigDao computeSpecConfigDaoImpl(final SessionFactory sessionFactory, - final @Qualifier("hardwareConfigDaoImpl") HardwareConfigDao hardwareConfigDaoImpl) { - return new ComputeSpecConfigDao(sessionFactory, hardwareConfigDaoImpl); + @Qualifier("computeEnvironmentConfigDaoImpl") + public ComputeEnvironmentConfigDao computeEnvironmentConfigDaoImpl(final SessionFactory sessionFactory, + final @Qualifier("hardwareConfigDaoImpl") HardwareConfigDao hardwareConfigDaoImpl) { + return new ComputeEnvironmentConfigDao(sessionFactory, hardwareConfigDaoImpl); } } diff --git a/src/test/java/org/nrg/xnat/compute/config/HibernateConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateConfig.java index 699040f..472d9af 100644 --- a/src/test/java/org/nrg/xnat/compute/config/HibernateConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/HibernateConfig.java @@ -56,10 +56,10 @@ public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qual ConstraintConfigEntity.class, ConstraintEntity.class, ConstraintScopeEntity.class, - ComputeSpecConfigEntity.class, - ComputeSpecEntity.class, - ComputeSpecScopeEntity.class, - ComputeSpecHardwareOptionsEntity.class, + ComputeEnvironmentConfigEntity.class, + ComputeEnvironmentEntity.class, + ComputeEnvironmentScopeEntity.class, + ComputeEnvironmentHardwareOptionsEntity.class, HardwareConfigEntity.class, HardwareEntity.class, HardwareScopeEntity.class, diff --git a/src/test/java/org/nrg/xnat/compute/config/HibernateConstraintConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateConstraintConfigEntityServiceTestConfig.java index c1bc22b..b1f228c 100644 --- a/src/test/java/org/nrg/xnat/compute/config/HibernateConstraintConfigEntityServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/HibernateConstraintConfigEntityServiceTestConfig.java @@ -33,10 +33,10 @@ public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qual ConstraintConfigEntity.class, ConstraintEntity.class, ConstraintScopeEntity.class, - ComputeSpecConfigEntity.class, - ComputeSpecEntity.class, - ComputeSpecScopeEntity.class, - ComputeSpecHardwareOptionsEntity.class, + ComputeEnvironmentConfigEntity.class, + ComputeEnvironmentEntity.class, + ComputeEnvironmentScopeEntity.class, + ComputeEnvironmentHardwareOptionsEntity.class, HardwareConfigEntity.class, HardwareEntity.class, HardwareScopeEntity.class, diff --git a/src/test/java/org/nrg/xnat/compute/config/HibernateEntityServicesConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateEntityServicesConfig.java index 82aa41d..eb3ef86 100644 --- a/src/test/java/org/nrg/xnat/compute/config/HibernateEntityServicesConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/HibernateEntityServicesConfig.java @@ -1,13 +1,13 @@ package org.nrg.xnat.compute.config; import org.hibernate.SessionFactory; -import org.nrg.xnat.compute.repositories.ComputeSpecConfigDao; +import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; import org.nrg.xnat.compute.repositories.ConstraintConfigDao; import org.nrg.xnat.compute.repositories.HardwareConfigDao; -import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; import org.nrg.xnat.compute.services.ConstraintConfigEntityService; import org.nrg.xnat.compute.services.HardwareConfigEntityService; -import org.nrg.xnat.compute.services.impl.HibernateComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.impl.HibernateComputeEnvironmentConfigEntityService; import org.nrg.xnat.compute.services.impl.HibernateConstraintConfigEntityService; import org.nrg.xnat.compute.services.impl.HibernateHardwareConfigEntityService; import org.springframework.context.annotation.Bean; @@ -36,16 +36,16 @@ public HardwareConfigDao hardwareConfigDao(final SessionFactory sessionFactory) } @Bean - public ComputeSpecConfigDao computeSpecConfigDao(final SessionFactory sessionFactory, - final HardwareConfigDao hardwareConfigDao) { - return new ComputeSpecConfigDao(sessionFactory, hardwareConfigDao); + public ComputeEnvironmentConfigDao computeEnvironmentConfigDao(final SessionFactory sessionFactory, + final HardwareConfigDao hardwareConfigDao) { + return new ComputeEnvironmentConfigDao(sessionFactory, hardwareConfigDao); } @Bean - public ComputeSpecConfigEntityService computeSpecConfigEntityService(final ComputeSpecConfigDao computeSpecConfigDao, - final HardwareConfigDao hardwareConfigDao) { - return new HibernateComputeSpecConfigEntityService( - computeSpecConfigDao, + public ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService(final ComputeEnvironmentConfigDao computeEnvironmentConfigDao, + final HardwareConfigDao hardwareConfigDao) { + return new HibernateComputeEnvironmentConfigEntityService( + computeEnvironmentConfigDao, hardwareConfigDao ); } diff --git a/src/test/java/org/nrg/xnat/compute/config/HibernateHardwareConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateHardwareConfigEntityServiceTestConfig.java index 49afb29..71efe1b 100644 --- a/src/test/java/org/nrg/xnat/compute/config/HibernateHardwareConfigEntityServiceTestConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/HibernateHardwareConfigEntityServiceTestConfig.java @@ -33,10 +33,10 @@ public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qual ConstraintConfigEntity.class, ConstraintEntity.class, ConstraintScopeEntity.class, - ComputeSpecConfigEntity.class, - ComputeSpecEntity.class, - ComputeSpecScopeEntity.class, - ComputeSpecHardwareOptionsEntity.class, + ComputeEnvironmentConfigEntity.class, + ComputeEnvironmentEntity.class, + ComputeEnvironmentScopeEntity.class, + ComputeEnvironmentHardwareOptionsEntity.class, HardwareConfigEntity.class, HardwareEntity.class, HardwareScopeEntity.class, diff --git a/src/test/java/org/nrg/xnat/compute/config/MockConfig.java b/src/test/java/org/nrg/xnat/compute/config/MockConfig.java index 6117afc..11ff2ec 100644 --- a/src/test/java/org/nrg/xnat/compute/config/MockConfig.java +++ b/src/test/java/org/nrg/xnat/compute/config/MockConfig.java @@ -2,7 +2,7 @@ import org.mockito.Mockito; import org.nrg.framework.services.SerializerService; -import org.nrg.xnat.compute.repositories.ComputeSpecConfigDao; +import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; import org.nrg.xnat.compute.repositories.HardwareConfigDao; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.RoleServiceI; @@ -61,14 +61,14 @@ public ConstraintConfigService mockPlacementConstraintConfigService() { } @Bean - public ComputeSpecConfigService mockComputeSpecConfigService() { - return Mockito.mock(ComputeSpecConfigService.class); + public ComputeEnvironmentConfigService mockComputeEnvironmentConfigService() { + return Mockito.mock(ComputeEnvironmentConfigService.class); } @Bean - @Qualifier("mockComputeSpecConfigEntityService") - public ComputeSpecConfigEntityService mockComputeSpecConfigEntityService() { - return Mockito.mock(ComputeSpecConfigEntityService.class); + @Qualifier("mockComputeEnvironmentConfigEntityService") + public ComputeEnvironmentConfigEntityService mockComputeEnvironmentConfigEntityService() { + return Mockito.mock(ComputeEnvironmentConfigEntityService.class); } @Bean @@ -89,9 +89,9 @@ public HardwareConfigDao mockHardwareConfigDao() { } @Bean - @Qualifier("mockComputeSpecConfigDao") - public ComputeSpecConfigDao mockComputeSpecConfigDao() { - return Mockito.mock(ComputeSpecConfigDao.class); + @Qualifier("mockComputeEnvironmentConfigDao") + public ComputeEnvironmentConfigDao mockComputeEnvironmentConfigDao() { + return Mockito.mock(ComputeEnvironmentConfigDao.class); } @Bean diff --git a/src/test/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApiTest.java b/src/test/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApiTest.java similarity index 66% rename from src/test/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApiTest.java rename to src/test/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApiTest.java index 56d25fb..bc4a678 100644 --- a/src/test/java/org/nrg/xnat/compute/rest/ComputeSpecConfigsApiTest.java +++ b/src/test/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApiTest.java @@ -6,9 +6,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.nrg.xnat.compute.config.ComputeSpecConfigsApiConfig; -import org.nrg.xnat.compute.models.ComputeSpecConfig; -import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.config.ComputeEnvironmentConfigsApiConfig; +import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; import org.nrg.xdat.security.services.RoleServiceI; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xft.security.UserI; @@ -35,20 +35,20 @@ @WebAppConfiguration @RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {ComputeSpecConfigsApiConfig.class}) -public class ComputeSpecConfigsApiTest { +@ContextConfiguration(classes = {ComputeEnvironmentConfigsApiConfig.class}) +public class ComputeEnvironmentConfigsApiTest { @Autowired private WebApplicationContext wac; @Autowired private ObjectMapper mapper; @Autowired private RoleServiceI mockRoleService; @Autowired private UserManagementServiceI mockUserManagementService; - @Autowired private ComputeSpecConfigService mockComputeSpecConfigService; + @Autowired private ComputeEnvironmentConfigService mockComputeEnvironmentConfigService; private MockMvc mockMvc; private UserI mockUser; private Authentication mockAuthentication; - private ComputeSpecConfig computeSpecConfig; + private ComputeEnvironmentConfig computeEnvironmentConfig; @Before public void before() throws Exception { @@ -63,9 +63,9 @@ public void before() throws Exception { when(mockRoleService.isSiteAdmin(mockUser)).thenReturn(true); mockAuthentication = new TestingAuthenticationToken(mockUser, mockUser.getPassword()); - // Setup the compute spec config - computeSpecConfig = new ComputeSpecConfig(); - computeSpecConfig.setId(1L); + // Setup the compute environment config + computeEnvironmentConfig = new ComputeEnvironmentConfig(); + computeEnvironmentConfig.setId(1L); } @After @@ -73,15 +73,15 @@ public void after() throws Exception { Mockito.reset( mockRoleService, mockUserManagementService, - mockComputeSpecConfigService, + mockComputeEnvironmentConfigService, mockUser ); } @Test - public void testGetAllComputeSpecConfigs() throws Exception { + public void testGetAllComputeEnvironmentConfigs() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/compute-spec-configs") + .get("/compute-environment-configs") .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); @@ -89,14 +89,14 @@ public void testGetAllComputeSpecConfigs() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); // Verify that the service was called - verify(mockComputeSpecConfigService, times(1)).getAll(); - verify(mockComputeSpecConfigService, never()).getByType(any()); + verify(mockComputeEnvironmentConfigService, times(1)).getAll(); + verify(mockComputeEnvironmentConfigService, never()).getByType(any()); } @Test - public void testGetComputeSpecConfigsByType() throws Exception { + public void testGetComputeEnvironmentConfigsByType() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/compute-spec-configs") + .get("/compute-environment-configs") .param("type", "JUPYTERHUB") .with(authentication(mockAuthentication)) .with(csrf()) @@ -105,51 +105,51 @@ public void testGetComputeSpecConfigsByType() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); // Verify that the service was called - verify(mockComputeSpecConfigService, never()).getAll(); - verify(mockComputeSpecConfigService, times(1)).getByType(any()); + verify(mockComputeEnvironmentConfigService, never()).getAll(); + verify(mockComputeEnvironmentConfigService, times(1)).getByType(any()); } @Test - public void testGetComputeSpecConfigById() throws Exception { + public void testGetComputeEnvironmentConfigById() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/compute-spec-configs/1") + .get("/compute-environment-configs/1") .accept(APPLICATION_JSON) .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); - when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); mockMvc.perform(request).andExpect(status().isOk()); // Verify that the service was called - verify(mockComputeSpecConfigService, times(1)).retrieve(any()); + verify(mockComputeEnvironmentConfigService, times(1)).retrieve(any()); } @Test - public void testGetComputeSpecConfigByIdNotFound() throws Exception { + public void testGetComputeEnvironmentConfigByIdNotFound() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/compute-spec-configs/1") + .get("/compute-environment-configs/1") .accept(APPLICATION_JSON) .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); - when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.empty()); + when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.empty()); mockMvc.perform(request).andExpect(status().isNotFound()); // Verify that the service was called - verify(mockComputeSpecConfigService, times(1)).retrieve(any()); + verify(mockComputeEnvironmentConfigService, times(1)).retrieve(any()); } @Test - public void testCreateComputeSpecConfig() throws Exception { + public void testCreateComputeEnvironmentConfig() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .post("/compute-spec-configs") + .post("/compute-environment-configs") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) - .content(mapper.writeValueAsString(new ComputeSpecConfig())) + .content(mapper.writeValueAsString(new ComputeEnvironmentConfig())) .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); @@ -157,35 +157,35 @@ public void testCreateComputeSpecConfig() throws Exception { mockMvc.perform(request).andExpect(status().isCreated()); // Verify that the service was called - verify(mockComputeSpecConfigService, times(1)).create(any()); + verify(mockComputeEnvironmentConfigService, times(1)).create(any()); } @Test - public void testUpdateComputeSpecConfig() throws Exception { + public void testUpdateComputeEnvironmentConfig() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/compute-spec-configs/1") + .put("/compute-environment-configs/1") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) - .content(mapper.writeValueAsString(computeSpecConfig)) + .content(mapper.writeValueAsString(computeEnvironmentConfig)) .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); - when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); mockMvc.perform(request).andExpect(status().isOk()); // Verify that the service was called - verify(mockComputeSpecConfigService, times(1)).update(eq(computeSpecConfig)); + verify(mockComputeEnvironmentConfigService, times(1)).update(eq(computeEnvironmentConfig)); } @Test(expected = NestedServletException.class) - public void testUpdateComputeSpecConfig_IdMismatch() throws Exception { + public void testUpdateComputeEnvironmentConfig_IdMismatch() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/compute-spec-configs/2") + .put("/compute-environment-configs/2") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) - .content(mapper.writeValueAsString(computeSpecConfig)) + .content(mapper.writeValueAsString(computeEnvironmentConfig)) .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); @@ -194,13 +194,13 @@ public void testUpdateComputeSpecConfig_IdMismatch() throws Exception { mockMvc.perform(request).andExpect(status().isBadRequest()); // Verify that the service was not called - verify(mockComputeSpecConfigService, never()).update(any()); + verify(mockComputeEnvironmentConfigService, never()).update(any()); } @Test - public void testDeleteComputeSpecConfig() throws Exception { + public void testDeleteComputeEnvironmentConfig() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete("/compute-spec-configs/1") + .delete("/compute-environment-configs/1") .with(authentication(mockAuthentication)) .with(csrf()) .with(testSecurityContext()); @@ -208,13 +208,13 @@ public void testDeleteComputeSpecConfig() throws Exception { mockMvc.perform(request).andExpect(status().isNoContent()); // Verify that the service was called - verify(mockComputeSpecConfigService, times(1)).delete(eq(1L)); + verify(mockComputeEnvironmentConfigService, times(1)).delete(eq(1L)); } @Test - public void testGetAvailableComputeSpecConfigs() throws Exception { + public void testGetAvailableComputeEnvironmentConfigs() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/compute-spec-configs/available") + .get("/compute-environment-configs/available") .param("user", mockUser.getLogin()) .param("project", "projectId") .param("type", "JUPYTERHUB") @@ -225,7 +225,7 @@ public void testGetAvailableComputeSpecConfigs() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); // Verify that the service was called - verify(mockComputeSpecConfigService, times(1)).getAvailable(eq(mockUser.getLogin()), eq("projectId"), eq(ComputeSpecConfig.ConfigType.JUPYTERHUB)); + verify(mockComputeEnvironmentConfigService, times(1)).getAvailable(eq(mockUser.getLogin()), eq("projectId"), eq(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB)); } diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigServiceTest.java similarity index 55% rename from src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigServiceTest.java rename to src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigServiceTest.java index 7355ff4..556f28a 100644 --- a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeSpecConfigServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigServiceTest.java @@ -3,7 +3,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.nrg.xnat.compute.config.DefaultComputeSpecConfigServiceTestConfig; +import org.nrg.xnat.compute.config.DefaultComputeEnvironmentConfigServiceTestConfig; import org.nrg.xnat.compute.models.*; import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; @@ -21,22 +21,22 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.*; import static org.nrg.framework.constants.Scope.*; -import static org.nrg.xnat.compute.models.ComputeSpecConfig.ConfigType.CONTAINER_SERVICE; -import static org.nrg.xnat.compute.models.ComputeSpecConfig.ConfigType.JUPYTERHUB; +import static org.nrg.xnat.compute.models.ComputeEnvironmentConfig.ConfigType.CONTAINER_SERVICE; +import static org.nrg.xnat.compute.models.ComputeEnvironmentConfig.ConfigType.JUPYTERHUB; import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; @RunWith(SpringJUnit4ClassRunner.class) @Transactional -@ContextConfiguration(classes = DefaultComputeSpecConfigServiceTestConfig.class) -public class DefaultComputeSpecConfigServiceTest { +@ContextConfiguration(classes = DefaultComputeEnvironmentConfigServiceTestConfig.class) +public class DefaultComputeEnvironmentConfigServiceTest { - @Autowired private DefaultComputeSpecConfigService defaultComputeSpecConfigService; + @Autowired private DefaultComputeEnvironmentConfigService defaultComputeEnvironmentConfigService; @Autowired private HardwareConfigEntityService hardwareConfigEntityService; - private ComputeSpecConfig computeSpecConfig1; - private ComputeSpecConfig computeSpecConfig2; - private ComputeSpecConfig computeSpecConfig3; - private ComputeSpecConfig computeSpecConfigInvalid; + private ComputeEnvironmentConfig computeEnvironmentConfig1; + private ComputeEnvironmentConfig computeEnvironmentConfig2; + private ComputeEnvironmentConfig computeEnvironmentConfig3; + private ComputeEnvironmentConfig computeEnvironmentConfigInvalid; private HardwareConfig hardwareConfig1; private HardwareConfig hardwareConfig2; @@ -50,9 +50,9 @@ public void before() { @DirtiesContext public void testExists() { // Test - ComputeSpecConfig created = defaultComputeSpecConfigService.create(computeSpecConfig1); + ComputeEnvironmentConfig created = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - boolean exists = defaultComputeSpecConfigService.exists(created.getId()); + boolean exists = defaultComputeEnvironmentConfigService.exists(created.getId()); // Verify assertTrue(exists); @@ -62,9 +62,9 @@ public void testExists() { @DirtiesContext public void testDoesNotExist() { // Test - ComputeSpecConfig created = defaultComputeSpecConfigService.create(computeSpecConfig1); + ComputeEnvironmentConfig created = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - boolean exists = defaultComputeSpecConfigService.exists(created.getId() + 1); + boolean exists = defaultComputeEnvironmentConfigService.exists(created.getId() + 1); // Verify assertFalse(exists); @@ -74,174 +74,174 @@ public void testDoesNotExist() { @DirtiesContext public void testGetDoesNotExist() { // Test - Optional computeSpecConfig = defaultComputeSpecConfigService.retrieve(1L); + Optional computeEnvironmentConfig = defaultComputeEnvironmentConfigService.retrieve(1L); // Verify - assertFalse(computeSpecConfig.isPresent()); + assertFalse(computeEnvironmentConfig.isPresent()); } @Test @DirtiesContext public void testGet() { // Test - ComputeSpecConfig created = defaultComputeSpecConfigService.create(computeSpecConfig1); + ComputeEnvironmentConfig created = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - Optional computeSpecConfig = defaultComputeSpecConfigService.retrieve(created.getId()); + Optional computeEnvironmentConfig = defaultComputeEnvironmentConfigService.retrieve(created.getId()); // Verify - assertTrue(computeSpecConfig.isPresent()); - assertEquals(created, computeSpecConfig.get()); + assertTrue(computeEnvironmentConfig.isPresent()); + assertEquals(created, computeEnvironmentConfig.get()); - assertEquals(computeSpecConfig1.getComputeSpec(), computeSpecConfig.get().getComputeSpec()); - assertEquals(computeSpecConfig1.getConfigTypes(), computeSpecConfig.get().getConfigTypes()); - assertEquals(computeSpecConfig1.getScopes(), computeSpecConfig.get().getScopes()); + assertEquals(computeEnvironmentConfig1.getComputeEnvironment(), computeEnvironmentConfig.get().getComputeEnvironment()); + assertEquals(computeEnvironmentConfig1.getConfigTypes(), computeEnvironmentConfig.get().getConfigTypes()); + assertEquals(computeEnvironmentConfig1.getScopes(), computeEnvironmentConfig.get().getScopes()); } @Test @DirtiesContext public void testGetAll() { // Test - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); commitTransaction(); - ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); commitTransaction(); - List computeSpecConfigs = defaultComputeSpecConfigService.getAll(); + List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAll(); // Verify - assertThat(computeSpecConfigs.size(), is(3)); - assertThat(computeSpecConfigs, hasItems(created1, created2, created3)); + assertThat(computeEnvironmentConfigs.size(), is(3)); + assertThat(computeEnvironmentConfigs, hasItems(created1, created2, created3)); } @Test @DirtiesContext public void testGetByType() { - // Create ComputeSpecConfigs - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + // Create ComputeEnvironmentConfigs + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); commitTransaction(); - ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); commitTransaction(); // Test - List computeSpecConfigs = defaultComputeSpecConfigService.getByType(CONTAINER_SERVICE); + List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getByType(CONTAINER_SERVICE); // Verify - assertThat(computeSpecConfigs.size(), is(2)); - assertThat(computeSpecConfigs, hasItems(created2, created3)); + assertThat(computeEnvironmentConfigs.size(), is(2)); + assertThat(computeEnvironmentConfigs, hasItems(created2, created3)); // Test - computeSpecConfigs = defaultComputeSpecConfigService.getByType(JUPYTERHUB); + computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getByType(JUPYTERHUB); // Verify - assertEquals(2, computeSpecConfigs.size()); - assertThat(computeSpecConfigs, containsInAnyOrder(created1, created3)); + assertEquals(2, computeEnvironmentConfigs.size()); + assertThat(computeEnvironmentConfigs, containsInAnyOrder(created1, created3)); } @Test @DirtiesContext public void testGetAvailable_WrongUser() { - // Create ComputeSpecConfigs - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + // Create ComputeEnvironmentConfigs + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); commitTransaction(); - ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); commitTransaction(); // Test - List computeSpecConfigs = defaultComputeSpecConfigService.getAvailable("User2", "Project1", null); + List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable("User2", "Project1", null); // Verify - assertThat(computeSpecConfigs.size(), is(1)); - assertThat(computeSpecConfigs, hasItems(created1)); + assertThat(computeEnvironmentConfigs.size(), is(1)); + assertThat(computeEnvironmentConfigs, hasItems(created1)); } @Test @DirtiesContext public void testGetAvailable_WrongProject() { - // Create ComputeSpecConfigs - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + // Create ComputeEnvironmentConfigs + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); commitTransaction(); - ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); commitTransaction(); // Test - List computeSpecConfigs = defaultComputeSpecConfigService.getAvailable("User1", "Project2"); + List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable("User1", "Project2"); // Verify - assertThat(computeSpecConfigs.size(), is(1)); - assertThat(computeSpecConfigs, hasItem(created1)); + assertThat(computeEnvironmentConfigs.size(), is(1)); + assertThat(computeEnvironmentConfigs, hasItem(created1)); } @Test @DirtiesContext public void testGetAvailable() { - // Create ComputeSpecConfigs - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + // Create ComputeEnvironmentConfigs + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); commitTransaction(); - ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); commitTransaction(); // Test - List computeSpecConfigs = defaultComputeSpecConfigService.getAvailable("User1", "Project1"); + List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable("User1", "Project1"); // Verify - assertThat(computeSpecConfigs.size(), is(2)); - assertThat(computeSpecConfigs, hasItems(created1, created2)); + assertThat(computeEnvironmentConfigs.size(), is(2)); + assertThat(computeEnvironmentConfigs, hasItems(created1, created2)); } @Test @DirtiesContext public void testGetAvailable_SpecificType() { - // Create ComputeSpecConfigs - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + // Create ComputeEnvironmentConfigs + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); commitTransaction(); - ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); commitTransaction(); // Test - List computeSpecConfigs = defaultComputeSpecConfigService.getAvailable("User1", "Project1", CONTAINER_SERVICE); + List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable("User1", "Project1", CONTAINER_SERVICE); // Verify - assertThat(computeSpecConfigs.size(), is(1)); - assertThat(computeSpecConfigs, hasItems(created2)); + assertThat(computeEnvironmentConfigs.size(), is(1)); + assertThat(computeEnvironmentConfigs, hasItems(created2)); } @Test @DirtiesContext public void testIsAvailable() { - // Create ComputeSpecConfigs - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + // Create ComputeEnvironmentConfigs + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); commitTransaction(); - ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); commitTransaction(); // Test - boolean result = defaultComputeSpecConfigService.isAvailable("User1", "Project1", created1.getId()); + boolean result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project1", created1.getId()); // Verify assertTrue(result); // Test - result = defaultComputeSpecConfigService.isAvailable("User1", "Project1", created2.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project1", created2.getId()); // Verify assertTrue(result); // Test - result = defaultComputeSpecConfigService.isAvailable("User1", "Project1", created3.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project1", created3.getId()); // Verify assertFalse(result); @@ -250,28 +250,28 @@ public void testIsAvailable() { @Test @DirtiesContext public void testNotAvailable_WrongUser() { - // Create ComputeSpecConfigs - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + // Create ComputeEnvironmentConfigs + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); commitTransaction(); - ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); commitTransaction(); // Test - boolean result = defaultComputeSpecConfigService.isAvailable("User2", "Project1", created1.getId()); + boolean result = defaultComputeEnvironmentConfigService.isAvailable("User2", "Project1", created1.getId()); // Verify assertTrue(result); // Test - result = defaultComputeSpecConfigService.isAvailable("User2", "Project1", created2.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable("User2", "Project1", created2.getId()); // Verify assertFalse(result); // Test - result = defaultComputeSpecConfigService.isAvailable("User2", "Project1", created3.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable("User2", "Project1", created3.getId()); // Verify assertFalse(result); @@ -280,28 +280,28 @@ public void testNotAvailable_WrongUser() { @Test @DirtiesContext public void testNotAvailable_WrongProject() { - // Create ComputeSpecConfigs - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + // Create ComputeEnvironmentConfigs + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); commitTransaction(); - ComputeSpecConfig created3 = defaultComputeSpecConfigService.create(computeSpecConfig3); + ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); commitTransaction(); // Test - boolean result = defaultComputeSpecConfigService.isAvailable("User1", "Project2", created1.getId()); + boolean result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project2", created1.getId()); // Verify assertTrue(result); // Test - result = defaultComputeSpecConfigService.isAvailable("User1", "Project2", created2.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project2", created2.getId()); // Verify assertFalse(result); // Test - result = defaultComputeSpecConfigService.isAvailable("User1", "Project2", created3.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project2", created3.getId()); // Verify assertFalse(result); @@ -318,11 +318,11 @@ public void testCreate_AllowAllHardware() { hardwareConfig1.setId(hardwareConfigEntity1.getId()); hardwareConfig2.setId(hardwareConfigEntity2.getId()); - // Next create compute spec config - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + // Next create compute environment config + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - // Verify that all hardware configs are associated with the compute spec config + // Verify that all hardware configs are associated with the compute environment config assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(2)); assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2)); } @@ -338,11 +338,11 @@ public void testCreate_SelectHardware() { hardwareConfig1.setId(hardwareConfigEntity1.getId()); hardwareConfig2.setId(hardwareConfigEntity2.getId()); - // Next create compute spec config - ComputeSpecConfig created2 = defaultComputeSpecConfigService.create(computeSpecConfig2); + // Next create compute environment config + ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); commitTransaction(); - // Verify that only the selected hardware config is associated with the compute spec config + // Verify that only the selected hardware config is associated with the compute environment config assertThat(created2.getHardwareOptions().getHardwareConfigs().size(), is(1)); assertThat(created2.getHardwareOptions().getHardwareConfigs(), hasItem(hardwareConfig2)); } @@ -358,35 +358,35 @@ public void testUpdate() throws NotFoundException { hardwareConfig1.setId(hardwareConfigEntity1.getId()); hardwareConfig2.setId(hardwareConfigEntity2.getId()); - // Next create compute spec config - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + // Next create compute environment config + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - // Verify that all hardware configs are associated with the compute spec config + // Verify that all hardware configs are associated with the compute environment config assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(2)); assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2)); - // Update the compute spec config + // Update the compute environment config created1.getHardwareOptions().setAllowAllHardware(false); created1.getHardwareOptions().getHardwareConfigs().remove(hardwareConfig1); // Update other fields - created1.getComputeSpec().setName("UpdatedName"); - created1.getComputeSpec().setImage("UpdatedImage"); - created1.getComputeSpec().getEnvironmentVariables().add(new EnvironmentVariable("UpdatedKey", "UpdatedValue")); + created1.getComputeEnvironment().setName("UpdatedName"); + created1.getComputeEnvironment().setImage("UpdatedImage"); + created1.getComputeEnvironment().getEnvironmentVariables().add(new EnvironmentVariable("UpdatedKey", "UpdatedValue")); - // Update the compute spec config - defaultComputeSpecConfigService.update(created1); + // Update the compute environment config + defaultComputeEnvironmentConfigService.update(created1); commitTransaction(); - // Verify that only the selected hardware config is associated with the compute spec config + // Verify that only the selected hardware config is associated with the compute environment config assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(1)); assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItem(hardwareConfig2)); // Verify that the other fields were updated - assertThat(created1.getComputeSpec().getName(), is("UpdatedName")); - assertThat(created1.getComputeSpec().getImage(), is("UpdatedImage")); - assertThat(created1.getComputeSpec().getEnvironmentVariables(), hasItem(new EnvironmentVariable("UpdatedKey", "UpdatedValue"))); + assertThat(created1.getComputeEnvironment().getName(), is("UpdatedName")); + assertThat(created1.getComputeEnvironment().getImage(), is("UpdatedImage")); + assertThat(created1.getComputeEnvironment().getEnvironmentVariables(), hasItem(new EnvironmentVariable("UpdatedKey", "UpdatedValue"))); } @Test @@ -400,39 +400,39 @@ public void testDelete() throws Exception { hardwareConfig1.setId(hardwareConfigEntity1.getId()); hardwareConfig2.setId(hardwareConfigEntity2.getId()); - // Next create compute spec config - ComputeSpecConfig created1 = defaultComputeSpecConfigService.create(computeSpecConfig1); + // Next create compute environment config + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); - // Verify that all hardware configs are associated with the compute spec config + // Verify that all hardware configs are associated with the compute environment config assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(2)); assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2)); hardwareConfigEntity1 = hardwareConfigEntityService.retrieve(hardwareConfigEntity1.getId()); hardwareConfigEntity2 = hardwareConfigEntityService.retrieve(hardwareConfigEntity2.getId()); - assertThat(hardwareConfigEntity1.getComputeSpecHardwareOptions().size(), is(1)); - assertThat(hardwareConfigEntity2.getComputeSpecHardwareOptions().size(), is(1)); + assertThat(hardwareConfigEntity1.getComputeEnvironmentHardwareOptions().size(), is(1)); + assertThat(hardwareConfigEntity2.getComputeEnvironmentHardwareOptions().size(), is(1)); - // Delete the compute spec config - defaultComputeSpecConfigService.delete(created1.getId()); + // Delete the compute environment config + defaultComputeEnvironmentConfigService.delete(created1.getId()); commitTransaction(); - // Verify that the compute spec config was deleted - assertThat(defaultComputeSpecConfigService.exists(created1.getId()), is(false)); + // Verify that the compute environment config was deleted + assertThat(defaultComputeEnvironmentConfigService.exists(created1.getId()), is(false)); // Verify that the hardware config entities were deleted hardwareConfigEntity1 = hardwareConfigEntityService.retrieve(hardwareConfigEntity1.getId()); hardwareConfigEntity2 = hardwareConfigEntityService.retrieve(hardwareConfigEntity2.getId()); - assertThat(hardwareConfigEntity1.getComputeSpecHardwareOptions().size(), is(0)); - assertThat(hardwareConfigEntity2.getComputeSpecHardwareOptions().size(), is(0)); + assertThat(hardwareConfigEntity1.getComputeEnvironmentHardwareOptions().size(), is(0)); + assertThat(hardwareConfigEntity2.getComputeEnvironmentHardwareOptions().size(), is(0)); } @Test public void testValidate() { try { - defaultComputeSpecConfigService.validate(computeSpecConfigInvalid); + defaultComputeEnvironmentConfigService.validate(computeEnvironmentConfigInvalid); fail("Expected exception to be thrown"); } catch (IllegalArgumentException e) { // Verify that the exception message contains the expected validation errors @@ -579,156 +579,156 @@ public void createDummyConfigs() { .scopes(hardwareScopes2) .build(); - // Setup first compute spec - ComputeSpec computeSpec1 = ComputeSpec.builder() + // Setup first compute environment + ComputeEnvironment computeEnvironment1 = ComputeEnvironment.builder() .name("Jupyter Datascience Notebook") .image("jupyter/datascience-notebook:hub-3.0.0") .environmentVariables(new ArrayList<>()) .mounts(new ArrayList<>()) .build(); - ComputeSpecScope computeSpecSiteScope1 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentSiteScope1 = ComputeEnvironmentScope.builder() .scope(Site) .enabled(true) .ids(new HashSet<>(Collections.emptyList())) .build(); - ComputeSpecScope computeSpecProjectScope1 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentProjectScope1 = ComputeEnvironmentScope.builder() .scope(Project) .enabled(true) .ids(new HashSet<>(Collections.emptyList())) .build(); - ComputeSpecScope computeSpecUserScope1 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentUserScope1 = ComputeEnvironmentScope.builder() .scope(User) .enabled(true) .ids(new HashSet<>(Collections.emptyList())) .build(); - Map computeSpecScopes1 = new HashMap<>(); - computeSpecScopes1.put(Site, computeSpecSiteScope1); - computeSpecScopes1.put(Project, computeSpecProjectScope1); - computeSpecScopes1.put(User, computeSpecUserScope1); + Map computeEnvironmentScopes1 = new HashMap<>(); + computeEnvironmentScopes1.put(Site, computeEnvironmentSiteScope1); + computeEnvironmentScopes1.put(Project, computeEnvironmentProjectScope1); + computeEnvironmentScopes1.put(User, computeEnvironmentUserScope1); - ComputeSpecHardwareOptions computeSpecHardwareOptions1 = ComputeSpecHardwareOptions.builder() + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions1 = ComputeEnvironmentHardwareOptions.builder() .allowAllHardware(true) .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig1, hardwareConfig2))) .build(); - computeSpecConfig1 = ComputeSpecConfig.builder() - .configTypes(new HashSet<>(Collections.singletonList(ComputeSpecConfig.ConfigType.JUPYTERHUB))) - .computeSpec(computeSpec1) - .scopes(computeSpecScopes1) - .hardwareOptions(computeSpecHardwareOptions1) + computeEnvironmentConfig1 = ComputeEnvironmentConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB))) + .computeEnvironment(computeEnvironment1) + .scopes(computeEnvironmentScopes1) + .hardwareOptions(computeEnvironmentHardwareOptions1) .build(); - // Setup second compute spec - ComputeSpec computeSpec2 = ComputeSpec.builder() + // Setup second compute environment + ComputeEnvironment computeEnvironment2 = ComputeEnvironment.builder() .name("XNAT Datascience Notebook") .image("xnat/datascience-notebook:latest") .environmentVariables(new ArrayList<>()) .mounts(new ArrayList<>()) .build(); - ComputeSpecScope computeSpecSiteScope2 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentSiteScope2 = ComputeEnvironmentScope.builder() .scope(Site) .enabled(true) .ids(new HashSet<>(Collections.emptyList())) .build(); - ComputeSpecScope computeSpecProjectScope2 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentProjectScope2 = ComputeEnvironmentScope.builder() .scope(Project) .enabled(false) .ids(new HashSet<>(Collections.singletonList("Project1"))) .build(); - ComputeSpecScope computeSpecUserScope2 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentUserScope2 = ComputeEnvironmentScope.builder() .scope(User) .enabled(false) .ids(new HashSet<>(Collections.singletonList("User1"))) .build(); - Map computeSpecScopes2 = new HashMap<>(); - computeSpecScopes2.put(Site, computeSpecSiteScope2); - computeSpecScopes2.put(Project, computeSpecProjectScope2); - computeSpecScopes2.put(User, computeSpecUserScope2); + Map computeEnvironmentScopes2 = new HashMap<>(); + computeEnvironmentScopes2.put(Site, computeEnvironmentSiteScope2); + computeEnvironmentScopes2.put(Project, computeEnvironmentProjectScope2); + computeEnvironmentScopes2.put(User, computeEnvironmentUserScope2); - ComputeSpecHardwareOptions computeSpecHardwareOptions2 = ComputeSpecHardwareOptions.builder() + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions2 = ComputeEnvironmentHardwareOptions.builder() .allowAllHardware(false) .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig2))) .build(); - computeSpecConfig2 = ComputeSpecConfig.builder() + computeEnvironmentConfig2 = ComputeEnvironmentConfig.builder() .id(2L) .configTypes(new HashSet<>(Collections.singletonList(CONTAINER_SERVICE))) - .computeSpec(computeSpec2) - .scopes(computeSpecScopes2) - .hardwareOptions(computeSpecHardwareOptions2) + .computeEnvironment(computeEnvironment2) + .scopes(computeEnvironmentScopes2) + .hardwareOptions(computeEnvironmentHardwareOptions2) .build(); - // Setup third compute spec - ComputeSpec computeSpec3 = ComputeSpec.builder() + // Setup third compute environment + ComputeEnvironment computeEnvironment3 = ComputeEnvironment.builder() .name("MATLAB Datascience Notebook") .image("matlab/datascience-notebook:latest") .environmentVariables(new ArrayList<>()) .mounts(new ArrayList<>()) .build(); - ComputeSpecScope computeSpecSiteScope3 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentSiteScope3 = ComputeEnvironmentScope.builder() .scope(Site) .enabled(false) .ids(new HashSet<>(Collections.emptyList())) .build(); - ComputeSpecScope computeSpecProjectScope3 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentProjectScope3 = ComputeEnvironmentScope.builder() .scope(Project) .enabled(true) .ids(new HashSet<>()) .build(); - ComputeSpecScope computeSpecUserScope3 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentUserScope3 = ComputeEnvironmentScope.builder() .scope(User) .enabled(true) .ids(new HashSet<>()) .build(); - Map computeSpecScopes3 = new HashMap<>(); - computeSpecScopes3.put(Site, computeSpecSiteScope3); - computeSpecScopes3.put(Project, computeSpecProjectScope3); - computeSpecScopes3.put(User, computeSpecUserScope3); + Map computeEnvironmentScopes3 = new HashMap<>(); + computeEnvironmentScopes3.put(Site, computeEnvironmentSiteScope3); + computeEnvironmentScopes3.put(Project, computeEnvironmentProjectScope3); + computeEnvironmentScopes3.put(User, computeEnvironmentUserScope3); - ComputeSpecHardwareOptions computeSpecHardwareOptions3 = ComputeSpecHardwareOptions.builder() + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions3 = ComputeEnvironmentHardwareOptions.builder() .allowAllHardware(true) .hardwareConfigs(new HashSet<>()) .build(); - computeSpecConfig3 = ComputeSpecConfig.builder() + computeEnvironmentConfig3 = ComputeEnvironmentConfig.builder() .configTypes(new HashSet<>(Arrays.asList(CONTAINER_SERVICE, JUPYTERHUB))) - .computeSpec(computeSpec3) - .scopes(computeSpecScopes3) - .hardwareOptions(computeSpecHardwareOptions3) + .computeEnvironment(computeEnvironment3) + .scopes(computeEnvironmentScopes3) + .hardwareOptions(computeEnvironmentHardwareOptions3) .build(); - // Setup invalid compute spec config - ComputeSpec computeSpecInvalid = ComputeSpec.builder() + // Setup invalid compute environment config + ComputeEnvironment computeEnvironmentInvalid = ComputeEnvironment.builder() .name("") // invalid .image("") // invalid .environmentVariables(new ArrayList<>()) .mounts(new ArrayList<>()) .build(); - Map computeSpecScopesInvalid = new HashMap<>(); // invalid, no scopes + Map computeEnvironmentScopesInvalid = new HashMap<>(); // invalid, no scopes - ComputeSpecHardwareOptions computeSpecHardwareOptionsInvalid = ComputeSpecHardwareOptions.builder() + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptionsInvalid = ComputeEnvironmentHardwareOptions.builder() .allowAllHardware(true) .hardwareConfigs(null) // invalid .build(); - computeSpecConfigInvalid = ComputeSpecConfig.builder() + computeEnvironmentConfigInvalid = ComputeEnvironmentConfig.builder() .configTypes(null) // invalid - .computeSpec(computeSpecInvalid) - .scopes(computeSpecScopesInvalid) - .hardwareOptions(computeSpecHardwareOptionsInvalid) + .computeEnvironment(computeEnvironmentInvalid) + .scopes(computeEnvironmentScopesInvalid) + .hardwareOptions(computeEnvironmentHardwareOptionsInvalid) .build(); } diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java index d906940..d709089 100644 --- a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java @@ -6,9 +6,9 @@ import org.nrg.xnat.compute.models.*; import org.nrg.framework.constants.Scope; import org.nrg.xnat.compute.config.DefaultHardwareConfigServiceTestConfig; -import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; +import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.services.ComputeSpecConfigEntityService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -29,10 +29,10 @@ public class DefaultHardwareConfigServiceTest { @Autowired private DefaultHardwareConfigService defaultHardwareConfigService; - @Autowired private ComputeSpecConfigEntityService computeSpecConfigEntityService; + @Autowired private ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService; - private ComputeSpecConfig computeSpecConfig1; - private ComputeSpecConfig computeSpecConfig2; + private ComputeEnvironmentConfig computeEnvironmentConfig1; + private ComputeEnvironmentConfig computeEnvironmentConfig2; private HardwareConfig hardwareConfig1; private HardwareConfig hardwareConfig2; private HardwareConfig hardwareConfigInvalid; @@ -109,9 +109,9 @@ public void testRetrieveAll() { @Test @DirtiesContext public void testCreate() { - // First, create the compute spec configs - ComputeSpecConfigEntity computeSpecConfigEntity1 = computeSpecConfigEntityService.create(ComputeSpecConfigEntity.fromPojo(computeSpecConfig1)); - ComputeSpecConfigEntity computeSpecConfigEntity2 = computeSpecConfigEntityService.create(ComputeSpecConfigEntity.fromPojo(computeSpecConfig2)); + // First, create the compute environment configs + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity1 = computeEnvironmentConfigEntityService.create(ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig1)); + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity2 = computeEnvironmentConfigEntityService.create(ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig2)); commitTransaction(); // Now create a hardware config @@ -120,14 +120,14 @@ public void testCreate() { HardwareConfig created2 = defaultHardwareConfigService.create(hardwareConfig2); commitTransaction(); - // Then retrieve the compute spec configs - computeSpecConfigEntity1 = computeSpecConfigEntityService.retrieve(computeSpecConfigEntity1.getId()); - computeSpecConfigEntity2 = computeSpecConfigEntityService.retrieve(computeSpecConfigEntity2.getId()); + // Then retrieve the compute environment configs + computeEnvironmentConfigEntity1 = computeEnvironmentConfigEntityService.retrieve(computeEnvironmentConfigEntity1.getId()); + computeEnvironmentConfigEntity2 = computeEnvironmentConfigEntityService.retrieve(computeEnvironmentConfigEntity2.getId()); - // Verify that the hardware config were added only to the first compute spec config - assertThat(computeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().size(), is(2)); - assertThat(computeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().size(), is(0)); - assertThat(computeSpecConfigEntity1 + // Verify that the hardware config were added only to the first compute environment config + assertThat(computeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().size(), is(2)); + assertThat(computeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().size(), is(0)); + assertThat(computeEnvironmentConfigEntity1 .getHardwareOptions() .getHardwareConfigs().stream() .map(HardwareConfigEntity::toPojo) @@ -380,90 +380,90 @@ public void createDummyConfigsAndEntities() { .scopes(hardwareScopesInvalid) .build(); - // Setup first compute spec - ComputeSpec computeSpec1 = ComputeSpec.builder() + // Setup first compute environment + ComputeEnvironment computeEnvironment1 = ComputeEnvironment.builder() .name("Jupyter Datascience Notebook") .image("jupyter/datascience-notebook:hub-3.0.0") .environmentVariables(new ArrayList<>()) .mounts(new ArrayList<>()) .build(); - ComputeSpecScope computeSpecSiteScope1 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentSiteScope1 = ComputeEnvironmentScope.builder() .scope(Site) .enabled(true) .ids(new HashSet<>(Collections.emptyList())) .build(); - ComputeSpecScope computeSpecProjectScope1 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentProjectScope1 = ComputeEnvironmentScope.builder() .scope(Project) .enabled(true) .ids(new HashSet<>(Collections.emptyList())) .build(); - ComputeSpecScope computeSpecUserScope1 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentUserScope1 = ComputeEnvironmentScope.builder() .scope(User) .enabled(true) .ids(new HashSet<>(Collections.emptyList())) .build(); - Map computeSpecScopes1 = new HashMap<>(); - computeSpecScopes1.put(Site, computeSpecSiteScope1); - computeSpecScopes1.put(Project, computeSpecProjectScope1); - computeSpecScopes1.put(User, computeSpecUserScope1); + Map computeEnvironmentScopes1 = new HashMap<>(); + computeEnvironmentScopes1.put(Site, computeEnvironmentSiteScope1); + computeEnvironmentScopes1.put(Project, computeEnvironmentProjectScope1); + computeEnvironmentScopes1.put(User, computeEnvironmentUserScope1); - ComputeSpecHardwareOptions computeSpecHardwareOptions1 = ComputeSpecHardwareOptions.builder() + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions1 = ComputeEnvironmentHardwareOptions.builder() .allowAllHardware(true) .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig1, hardwareConfig2))) .build(); - computeSpecConfig1 = ComputeSpecConfig.builder() - .configTypes(new HashSet<>(Collections.singletonList(ComputeSpecConfig.ConfigType.JUPYTERHUB))) - .computeSpec(computeSpec1) - .scopes(computeSpecScopes1) - .hardwareOptions(computeSpecHardwareOptions1) + computeEnvironmentConfig1 = ComputeEnvironmentConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB))) + .computeEnvironment(computeEnvironment1) + .scopes(computeEnvironmentScopes1) + .hardwareOptions(computeEnvironmentHardwareOptions1) .build(); - // Setup second compute spec - ComputeSpec computeSpec2 = ComputeSpec.builder() + // Setup second compute environment + ComputeEnvironment computeEnvironment2 = ComputeEnvironment.builder() .name("XNAT Datascience Notebook") .image("xnat/datascience-notebook:latest") .environmentVariables(new ArrayList<>()) .mounts(new ArrayList<>()) .build(); - ComputeSpecScope computeSpecSiteScope2 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentSiteScope2 = ComputeEnvironmentScope.builder() .scope(Site) .enabled(true) .ids(new HashSet<>(Collections.emptyList())) .build(); - ComputeSpecScope computeSpecProjectScope2 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentProjectScope2 = ComputeEnvironmentScope.builder() .scope(Project) .enabled(false) .ids(new HashSet<>(Collections.singletonList("Project1"))) .build(); - ComputeSpecScope computeSpecUserScope2 = ComputeSpecScope.builder() + ComputeEnvironmentScope computeEnvironmentUserScope2 = ComputeEnvironmentScope.builder() .scope(User) .enabled(false) .ids(new HashSet<>(Collections.singletonList("User1"))) .build(); - Map computeSpecScopes2 = new HashMap<>(); - computeSpecScopes2.put(Site, computeSpecSiteScope2); - computeSpecScopes2.put(Project, computeSpecProjectScope2); - computeSpecScopes2.put(User, computeSpecUserScope2); + Map computeEnvironmentScopes2 = new HashMap<>(); + computeEnvironmentScopes2.put(Site, computeEnvironmentSiteScope2); + computeEnvironmentScopes2.put(Project, computeEnvironmentProjectScope2); + computeEnvironmentScopes2.put(User, computeEnvironmentUserScope2); - ComputeSpecHardwareOptions computeSpecHardwareOptions2 = ComputeSpecHardwareOptions.builder() + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions2 = ComputeEnvironmentHardwareOptions.builder() .allowAllHardware(false) .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig2))) .build(); - computeSpecConfig2 = ComputeSpecConfig.builder() - .configTypes(new HashSet<>(Collections.singletonList(ComputeSpecConfig.ConfigType.JUPYTERHUB))) - .computeSpec(computeSpec2) - .scopes(computeSpecScopes2) - .hardwareOptions(computeSpecHardwareOptions2) + computeEnvironmentConfig2 = ComputeEnvironmentConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB))) + .computeEnvironment(computeEnvironment2) + .scopes(computeEnvironmentScopes2) + .hardwareOptions(computeEnvironmentHardwareOptions2) .build(); } diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java index eb56d69..293d7b5 100644 --- a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java @@ -7,7 +7,7 @@ import org.mockito.Mockito; import org.nrg.xnat.compute.models.*; import org.nrg.xnat.compute.config.DefaultJobTemplateServiceTestConfig; -import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; import org.nrg.xnat.compute.services.ConstraintConfigService; import org.nrg.xnat.compute.services.HardwareConfigService; import org.springframework.beans.factory.annotation.Autowired; @@ -28,7 +28,7 @@ public class DefaultJobTemplateServiceTest { @Autowired private DefaultJobTemplateService jobTemplateService; - @Autowired private ComputeSpecConfigService mockComputeSpecConfigService; + @Autowired private ComputeEnvironmentConfigService mockComputeEnvironmentConfigService; @Autowired private HardwareConfigService mockHardwareConfigService; @Autowired private ConstraintConfigService mockConstraintConfigService; @@ -39,16 +39,16 @@ public void before() { @After public void after() { Mockito.reset( - mockComputeSpecConfigService, + mockComputeEnvironmentConfigService, mockHardwareConfigService, mockConstraintConfigService ); } @Test - public void testIsAvailable_ComputeSpecNotAvailable() { + public void testIsAvailable_ComputeEnvironmentNotAvailable() { // Setup - when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(false); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(false); when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); // Run @@ -61,7 +61,7 @@ public void testIsAvailable_ComputeSpecNotAvailable() { @Test public void testIsAvailable_HardwareNotAvailable() { // Setup - when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(true); when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(false); // Run @@ -72,9 +72,9 @@ public void testIsAvailable_HardwareNotAvailable() { } @Test - public void testIsAvailable_ComputeSpecAndHardwareNotAvailable() { + public void testIsAvailable_ComputeEnvironmentAndHardwareNotAvailable() { // Setup - when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(false); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(false); when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(false); // Run @@ -85,16 +85,16 @@ public void testIsAvailable_ComputeSpecAndHardwareNotAvailable() { } @Test - public void testIsAvailable_ComputeSpecConfigAllHardwareIsAvailable() { + public void testIsAvailable_ComputeEnvironmentConfigAllHardwareIsAvailable() { // Setup - when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(true); when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); - ComputeSpecConfig computeSpecConfig = ComputeSpecConfig.builder().build(); - ComputeSpecHardwareOptions hardwareOptions = ComputeSpecHardwareOptions.builder().build(); + ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().build(); + ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); hardwareOptions.setAllowAllHardware(true); - computeSpecConfig.setHardwareOptions(hardwareOptions); - when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + computeEnvironmentConfig.setHardwareOptions(hardwareOptions); + when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); // Run boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); @@ -104,18 +104,18 @@ public void testIsAvailable_ComputeSpecConfigAllHardwareIsAvailable() { } @Test - public void testIsAvailable_ComputeSpecConfigSpecificHardwareAllowed() { + public void testIsAvailable_ComputeEnvironmentConfigEnvironmentificHardwareAllowed() { // Setup - when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(true); when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); - ComputeSpecConfig computeSpecConfig = ComputeSpecConfig.builder().id(1L).build(); - ComputeSpecHardwareOptions hardwareOptions = ComputeSpecHardwareOptions.builder().build(); + ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().id(1L).build(); + ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); hardwareOptions.setAllowAllHardware(false); - computeSpecConfig.setHardwareOptions(hardwareOptions); + computeEnvironmentConfig.setHardwareOptions(hardwareOptions); HardwareConfig hardwareConfig = HardwareConfig.builder().id(1L).build(); hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); - when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); // Run boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); @@ -125,18 +125,18 @@ public void testIsAvailable_ComputeSpecConfigSpecificHardwareAllowed() { } @Test - public void testIsAvailable_ComputeSpecConfigSpecificHardwareNotAllowed() { + public void testIsAvailable_ComputeEnvironmentConfigEnvironmentificHardwareNotAllowed() { // Setup - when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(true); when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); - ComputeSpecConfig computeSpecConfig = ComputeSpecConfig.builder().id(1L).build(); - ComputeSpecHardwareOptions hardwareOptions = ComputeSpecHardwareOptions.builder().build(); + ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().id(1L).build(); + ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); hardwareOptions.setAllowAllHardware(false); - computeSpecConfig.setHardwareOptions(hardwareOptions); + computeEnvironmentConfig.setHardwareOptions(hardwareOptions); HardwareConfig hardwareConfig = HardwareConfig.builder().id(1L).build(); hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); - when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); // Run boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 2L); @@ -148,18 +148,18 @@ public void testIsAvailable_ComputeSpecConfigSpecificHardwareNotAllowed() { @Test public void testResolve() { // Setup - when(mockComputeSpecConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(true); when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); - ComputeSpecConfig computeSpecConfig = ComputeSpecConfig.builder().id(1L).build(); - ComputeSpec computeSpec = ComputeSpec.builder() + ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().id(1L).build(); + ComputeEnvironment computeEnvironment = ComputeEnvironment.builder() .name("JupyterHub Data Science Notebook") .image("jupyter/datascience-notebook:latest") .build(); - computeSpecConfig.setComputeSpec(computeSpec); - ComputeSpecHardwareOptions hardwareOptions = ComputeSpecHardwareOptions.builder().build(); + computeEnvironmentConfig.setComputeEnvironment(computeEnvironment); + ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); hardwareOptions.setAllowAllHardware(false); - computeSpecConfig.setHardwareOptions(hardwareOptions); + computeEnvironmentConfig.setHardwareOptions(hardwareOptions); HardwareConfig hardwareConfig = HardwareConfig.builder().id(1L).build(); Hardware hardware = Hardware.builder() .name("Standard") @@ -180,7 +180,7 @@ public void testResolve() { .constraint(constraint) .build(); - when(mockComputeSpecConfigService.retrieve(any())).thenReturn(Optional.of(computeSpecConfig)); + when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); when(mockHardwareConfigService.retrieve(any())).thenReturn(Optional.of(hardwareConfig)); when(mockConstraintConfigService.getAvailable(any())).thenReturn(Collections.singletonList(constraintConfig)); @@ -189,7 +189,7 @@ public void testResolve() { // Verify assertNotNull(jobTemplate); - assertEquals(computeSpecConfig.getComputeSpec(), jobTemplate.getComputeSpec()); + assertEquals(computeEnvironmentConfig.getComputeEnvironment(), jobTemplate.getComputeEnvironment()); assertEquals(hardware, jobTemplate.getHardware()); assertEquals(Collections.singletonList(constraint), jobTemplate.getConstraints()); } diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityServiceTest.java new file mode 100644 index 0000000..ddd97b6 --- /dev/null +++ b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityServiceTest.java @@ -0,0 +1,505 @@ +package org.nrg.xnat.compute.services.impl; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.xnat.compute.models.*; +import org.nrg.framework.constants.Scope; +import org.nrg.xnat.compute.config.HibernateComputeEnvironmentConfigEntityServiceTestConfig; +import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; +import org.nrg.xnat.compute.repositories.HardwareConfigDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.transaction.Transactional; +import java.util.*; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; +import static org.nrg.framework.constants.Scope.*; +import static org.nrg.xnat.compute.models.ComputeEnvironmentConfig.ConfigType.CONTAINER_SERVICE; +import static org.nrg.xnat.compute.models.ComputeEnvironmentConfig.ConfigType.JUPYTERHUB; +import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; + +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +@ContextConfiguration(classes = HibernateComputeEnvironmentConfigEntityServiceTestConfig.class) +public class HibernateComputeEnvironmentConfigEntityServiceTest { + + @Autowired private HibernateComputeEnvironmentConfigEntityService hibernateComputeEnvironmentConfigEntityService; + @Autowired @Qualifier("hardwareConfigDaoImpl") private HardwareConfigDao hardwareConfigDaoImpl; + @Autowired @Qualifier("computeEnvironmentConfigDaoImpl") private ComputeEnvironmentConfigDao computeEnvironmentConfigDaoImpl; + + private ComputeEnvironmentConfig computeEnvironmentConfig1; + private ComputeEnvironmentConfig computeEnvironmentConfig2; + private ComputeEnvironmentConfig computeEnvironmentConfig3; + + private HardwareConfig hardwareConfig1; + private HardwareConfig hardwareConfig2; + + @Before + public void before() { + hibernateComputeEnvironmentConfigEntityService.setDao(computeEnvironmentConfigDaoImpl); + createDummyConfigsAndEntities(); + } + + @After + public void after() { + Mockito.reset(); + } + + @Test + public void test() { + assertNotNull(hibernateComputeEnvironmentConfigEntityService); + assertNotNull(hardwareConfigDaoImpl); + assertNotNull(computeEnvironmentConfigDaoImpl); + } + + @Test + @DirtiesContext + public void testCreate() { + // Setup + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity1 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig1); + + // Execute + ComputeEnvironmentConfigEntity created = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity1); + commitTransaction(); + ComputeEnvironmentConfigEntity retrieved = hibernateComputeEnvironmentConfigEntityService.retrieve(created.getId()); + + // Verify + assertNotNull(retrieved); + assertThat(retrieved.toPojo(), is(created.toPojo())); + } + + @Test + @DirtiesContext + public void testAddHardwareConfigEntity() { + // Setup + HardwareConfigEntity hardwareConfigEntity1 = HardwareConfigEntity.fromPojo(hardwareConfig1); + HardwareConfigEntity hardwareConfigEntity2 = HardwareConfigEntity.fromPojo(hardwareConfig2); + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity1 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig1); + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity2 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig2); + + // Commit hardware configs + Long hardwareConfigEntity1_id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity1); + Long hardwareConfigEntity2_Id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity2); + + ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity1); + ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity2); + + commitTransaction(); + + // Verify + assertNotNull(hardwareConfigEntity1_id); + assertNotNull(hardwareConfigEntity2_Id); + assertNotNull(createdComputeEnvironmentConfigEntity1); + assertNotNull(createdComputeEnvironmentConfigEntity2); + + // Execute + // Add hardware configs to compute environment configs + hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity1_id); + hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity2_Id); + hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity2.getId(), hardwareConfigEntity1_id); + + commitTransaction(); + + // Verify + ComputeEnvironmentConfigEntity retrievedComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity1.getId()); + ComputeEnvironmentConfigEntity retrievedComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity2.getId()); + + assertNotNull(retrievedComputeEnvironmentConfigEntity1); + assertNotNull(retrievedComputeEnvironmentConfigEntity2); + assertEquals(2, retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); + assertEquals(1, retrievedComputeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); + assertTrue(retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); + assertTrue(retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity2_Id))); + assertTrue(retrievedComputeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); + } + + @Test + @DirtiesContext + public void testRemoveHardwareConfigEntity() { + // Setup + HardwareConfigEntity hardwareConfigEntity1 = HardwareConfigEntity.fromPojo(hardwareConfig1); + HardwareConfigEntity hardwareConfigEntity2 = HardwareConfigEntity.fromPojo(hardwareConfig2); + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity1 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig1); + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity2 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig2); + + // Commit hardware configs + Long hardwareConfigEntity1_id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity1); + Long hardwareConfigEntity2_Id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity2); + + ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity1); + ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity2); + + commitTransaction(); + + // Verify + assertNotNull(hardwareConfigEntity1_id); + assertNotNull(hardwareConfigEntity2_Id); + assertNotNull(createdComputeEnvironmentConfigEntity1); + assertNotNull(createdComputeEnvironmentConfigEntity2); + + // Execute + // Add hardware configs to compute environment configs + hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity1_id); + hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity2_Id); + hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity2.getId(), hardwareConfigEntity1_id); + + commitTransaction(); + + // Verify + ComputeEnvironmentConfigEntity retrievedComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity1.getId()); + ComputeEnvironmentConfigEntity retrievedComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity2.getId()); + + assertNotNull(retrievedComputeEnvironmentConfigEntity1); + assertNotNull(retrievedComputeEnvironmentConfigEntity2); + assertEquals(2, retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); + assertEquals(1, retrievedComputeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); + assertTrue(retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); + assertTrue(retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity2_Id))); + assertTrue(retrievedComputeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); + + // Remove hardware configs from compute environment configs + hibernateComputeEnvironmentConfigEntityService.removeHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity1_id); + hibernateComputeEnvironmentConfigEntityService.removeHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity2_Id); + + commitTransaction(); + + hibernateComputeEnvironmentConfigEntityService.removeHardwareConfigEntity(createdComputeEnvironmentConfigEntity2.getId(), hardwareConfigEntity1_id); + + commitTransaction(); + + // Verify + retrievedComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity1.getId()); + retrievedComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity2.getId()); + + assertNotNull(retrievedComputeEnvironmentConfigEntity1); + assertNotNull(retrievedComputeEnvironmentConfigEntity2); + assertEquals(0, retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); + assertEquals(0, retrievedComputeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); + } + + @Test + @DirtiesContext + public void testFindByType() { + // Setup + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity1 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig1); + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity2 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig2); + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity3 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig3); + + // Commit compute environment configs + ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity1); + ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity2); + ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity3 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity3); + + commitTransaction(); + + // Verify + assertNotNull(createdComputeEnvironmentConfigEntity1); + assertNotNull(createdComputeEnvironmentConfigEntity2); + assertNotNull(createdComputeEnvironmentConfigEntity3); + + // Execute + List retrievedComputeEnvironmentConfigEntities = hibernateComputeEnvironmentConfigEntityService.findByType(JUPYTERHUB); + + // Verify + assertNotNull(retrievedComputeEnvironmentConfigEntities); + assertEquals(2, retrievedComputeEnvironmentConfigEntities.size()); + assertEquals(createdComputeEnvironmentConfigEntity1.getId(), retrievedComputeEnvironmentConfigEntities.get(0).getId()); + assertEquals(createdComputeEnvironmentConfigEntity3.getId(), retrievedComputeEnvironmentConfigEntities.get(1).getId()); + + // Execute + retrievedComputeEnvironmentConfigEntities = hibernateComputeEnvironmentConfigEntityService.findByType(CONTAINER_SERVICE); + + // Verify + assertNotNull(retrievedComputeEnvironmentConfigEntities); + assertEquals(2, retrievedComputeEnvironmentConfigEntities.size()); + assertEquals(createdComputeEnvironmentConfigEntity2.getId(), retrievedComputeEnvironmentConfigEntities.get(0).getId()); + assertEquals(createdComputeEnvironmentConfigEntity3.getId(), retrievedComputeEnvironmentConfigEntities.get(1).getId()); + } + + public void createDummyConfigsAndEntities() { + // Setup hardware + Hardware hardware1 = Hardware.builder() + .name("Small") + .cpuReservation(2.0) + .cpuLimit(4.0) + .memoryReservation("4G") + .memoryLimit("8G") + .build(); + + // Setup hardware constraints + Constraint hardwareConstraint1 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Collections.singletonList("worker"))) + .build(); + + Constraint hardwareConstraint2 = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + hardware1.setConstraints(Arrays.asList(hardwareConstraint1, hardwareConstraint2)); + + // Setup hardware environment variables + EnvironmentVariable hardwareEnvironmentVariable1 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); + EnvironmentVariable hardwareEnvironmentVariable2 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); + + hardware1.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable1, hardwareEnvironmentVariable2)); + + // Setup hardware generic resources + GenericResource hardwareGenericResource1 = new GenericResource("nvidia.com/gpu", "2"); + GenericResource hardwareGenericResource2 = new GenericResource("fpga.com/fpga", "1"); + + hardware1.setGenericResources(Arrays.asList(hardwareGenericResource1, hardwareGenericResource2)); + + // Setup hardware scopes + HardwareScope hardwareSiteScope1 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope1 = HardwareScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope userHardwareScope1 = HardwareScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map hardwareScopes1 = new HashMap<>(); + hardwareScopes1.put(Site, hardwareSiteScope1); + hardwareScopes1.put(Project, hardwareProjectScope1); + hardwareScopes1.put(User, userHardwareScope1); + + // Setup a hardware config entity + hardwareConfig1 = HardwareConfig.builder() + .hardware(hardware1) + .scopes(hardwareScopes1) + .build(); + + // Setup second hardware config + Hardware hardware2 = Hardware.builder() + .name("Medium") + .cpuReservation(4.0) + .cpuLimit(4.0) + .memoryReservation("8G") + .memoryLimit("8G") + .build(); + + // Setup hardware constraints + Constraint hardwareConstraint3 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Collections.singletonList("worker"))) + .build(); + + Constraint hardwareConstraint4 = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + hardware2.setConstraints(Arrays.asList(hardwareConstraint3, hardwareConstraint4)); + + // Setup hardware environment variables + EnvironmentVariable hardwareEnvironmentVariable3 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); + EnvironmentVariable hardwareEnvironmentVariable4 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); + + hardware2.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable3, hardwareEnvironmentVariable4)); + + // Setup hardware generic resources + GenericResource hardwareGenericResource3 = new GenericResource("nvidia.com/gpu", "2"); + GenericResource hardwareGenericResource4 = new GenericResource("fpga.com/fpga", "1"); + + hardware2.setGenericResources(Arrays.asList(hardwareGenericResource3, hardwareGenericResource4)); + + // Setup hardware scopes + HardwareScope hardwareSiteScope2 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope2 = HardwareScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope userHardwareScope2 = HardwareScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map hardwareScopes2 = new HashMap<>(); + hardwareScopes2.put(Site, hardwareSiteScope2); + hardwareScopes2.put(Project, hardwareProjectScope2); + hardwareScopes2.put(User, userHardwareScope2); + + // Setup second hardware config entity + hardwareConfig2 = HardwareConfig.builder() + .hardware(hardware2) + .scopes(hardwareScopes2) + .build(); + + // Setup first compute environment + // Create mount first + Mount mount1 = Mount.builder() + .localPath("/home/jovyan/work") + .containerPath("/home/jovyan/work") + .readOnly(false) + .build(); + + Mount mount2 = Mount.builder() + .localPath("/tools/MATLAB/R2019b") + .containerPath("/tools/MATLAB/R2019b") + .readOnly(true) + .build(); + + ComputeEnvironment computeEnvironment1 = ComputeEnvironment.builder() + .name("Jupyter Datascience Notebook") + .image("jupyter/datascience-notebook:hub-3.0.0") + .environmentVariables(Collections.singletonList(new EnvironmentVariable("JUPYTER_ENABLE_LAB", "yes"))) + .mounts(Arrays.asList(mount1, mount2)) + .build(); + + ComputeEnvironmentScope computeEnvironmentSiteScope1 = ComputeEnvironmentScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeEnvironmentScope computeEnvironmentProjectScope1 = ComputeEnvironmentScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeEnvironmentScope computeEnvironmentUserScope1 = ComputeEnvironmentScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map computeEnvironmentScopes1 = new HashMap<>(); + computeEnvironmentScopes1.put(Site, computeEnvironmentSiteScope1); + computeEnvironmentScopes1.put(Project, computeEnvironmentProjectScope1); + computeEnvironmentScopes1.put(User, computeEnvironmentUserScope1); + + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions1 = ComputeEnvironmentHardwareOptions.builder() + .allowAllHardware(true) + .hardwareConfigs(new HashSet<>()) + .build(); + + computeEnvironmentConfig1 = ComputeEnvironmentConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(JUPYTERHUB))) + .computeEnvironment(computeEnvironment1) + .scopes(computeEnvironmentScopes1) + .hardwareOptions(computeEnvironmentHardwareOptions1) + .build(); + + // Setup second compute environment + ComputeEnvironment computeEnvironment2 = ComputeEnvironment.builder() + .name("XNAT Datascience Notebook") + .image("xnat/datascience-notebook:latest") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeEnvironmentScope computeEnvironmentSiteScope2 = ComputeEnvironmentScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeEnvironmentScope computeEnvironmentProjectScope2 = ComputeEnvironmentScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("Project1"))) + .build(); + + ComputeEnvironmentScope computeEnvironmentUserScope2 = ComputeEnvironmentScope.builder() + .scope(User) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("User1"))) + .build(); + + Map computeEnvironmentScopes2 = new HashMap<>(); + computeEnvironmentScopes2.put(Site, computeEnvironmentSiteScope2); + computeEnvironmentScopes2.put(Project, computeEnvironmentProjectScope2); + computeEnvironmentScopes2.put(User, computeEnvironmentUserScope2); + + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions2 = ComputeEnvironmentHardwareOptions.builder() + .allowAllHardware(false) + .hardwareConfigs(new HashSet<>()) + .build(); + + computeEnvironmentConfig2 = ComputeEnvironmentConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(CONTAINER_SERVICE))) + .computeEnvironment(computeEnvironment2) + .scopes(computeEnvironmentScopes2) + .hardwareOptions(computeEnvironmentHardwareOptions2) + .build(); + + // Setup third compute environment + ComputeEnvironment computeEnvironment3 = ComputeEnvironment.builder() + .name("MATLAB Datascience Notebook") + .image("matlab/datascience-notebook:latest") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeEnvironmentScope computeEnvironmentSiteScope3 = ComputeEnvironmentScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeEnvironmentScope computeEnvironmentProjectScope3 = ComputeEnvironmentScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("Project1"))) + .build(); + + ComputeEnvironmentScope computeEnvironmentUserScope3 = ComputeEnvironmentScope.builder() + .scope(User) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("User1"))) + .build(); + + Map computeEnvironmentScopes3 = new HashMap<>(); + computeEnvironmentScopes2.put(Site, computeEnvironmentSiteScope3); + computeEnvironmentScopes2.put(Project, computeEnvironmentProjectScope3); + computeEnvironmentScopes2.put(User, computeEnvironmentUserScope3); + + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions3 = ComputeEnvironmentHardwareOptions.builder() + .allowAllHardware(false) + .hardwareConfigs(new HashSet<>()) + .build(); + + computeEnvironmentConfig3 = ComputeEnvironmentConfig.builder() + .configTypes(new HashSet<>(Arrays.asList(CONTAINER_SERVICE, JUPYTERHUB))) + .computeEnvironment(computeEnvironment3) + .scopes(computeEnvironmentScopes3) + .hardwareOptions(computeEnvironmentHardwareOptions3) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityServiceTest.java deleted file mode 100644 index 815b248..0000000 --- a/src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeSpecConfigEntityServiceTest.java +++ /dev/null @@ -1,505 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.nrg.xnat.compute.models.*; -import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.config.HibernateComputeSpecConfigEntityServiceTestConfig; -import org.nrg.xnat.compute.entities.ComputeSpecConfigEntity; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.repositories.ComputeSpecConfigDao; -import org.nrg.xnat.compute.repositories.HardwareConfigDao; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import javax.transaction.Transactional; -import java.util.*; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.*; -import static org.nrg.framework.constants.Scope.*; -import static org.nrg.xnat.compute.models.ComputeSpecConfig.ConfigType.CONTAINER_SERVICE; -import static org.nrg.xnat.compute.models.ComputeSpecConfig.ConfigType.JUPYTERHUB; -import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; - -@RunWith(SpringJUnit4ClassRunner.class) -@Transactional -@ContextConfiguration(classes = HibernateComputeSpecConfigEntityServiceTestConfig.class) -public class HibernateComputeSpecConfigEntityServiceTest { - - @Autowired private HibernateComputeSpecConfigEntityService hibernateComputeSpecConfigEntityService; - @Autowired @Qualifier("hardwareConfigDaoImpl") private HardwareConfigDao hardwareConfigDaoImpl; - @Autowired @Qualifier("computeSpecConfigDaoImpl") private ComputeSpecConfigDao computeSpecConfigDaoImpl; - - private ComputeSpecConfig computeSpecConfig1; - private ComputeSpecConfig computeSpecConfig2; - private ComputeSpecConfig computeSpecConfig3; - - private HardwareConfig hardwareConfig1; - private HardwareConfig hardwareConfig2; - - @Before - public void before() { - hibernateComputeSpecConfigEntityService.setDao(computeSpecConfigDaoImpl); - createDummyConfigsAndEntities(); - } - - @After - public void after() { - Mockito.reset(); - } - - @Test - public void test() { - assertNotNull(hibernateComputeSpecConfigEntityService); - assertNotNull(hardwareConfigDaoImpl); - assertNotNull(computeSpecConfigDaoImpl); - } - - @Test - @DirtiesContext - public void testCreate() { - // Setup - ComputeSpecConfigEntity computeSpecConfigEntity1 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig1); - - // Execute - ComputeSpecConfigEntity created = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity1); - commitTransaction(); - ComputeSpecConfigEntity retrieved = hibernateComputeSpecConfigEntityService.retrieve(created.getId()); - - // Verify - assertNotNull(retrieved); - assertThat(retrieved.toPojo(), is(created.toPojo())); - } - - @Test - @DirtiesContext - public void testAddHardwareConfigEntity() { - // Setup - HardwareConfigEntity hardwareConfigEntity1 = HardwareConfigEntity.fromPojo(hardwareConfig1); - HardwareConfigEntity hardwareConfigEntity2 = HardwareConfigEntity.fromPojo(hardwareConfig2); - ComputeSpecConfigEntity computeSpecConfigEntity1 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig1); - ComputeSpecConfigEntity computeSpecConfigEntity2 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig2); - - // Commit hardware configs - Long hardwareConfigEntity1_id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity1); - Long hardwareConfigEntity2_Id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity2); - - ComputeSpecConfigEntity createdComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity1); - ComputeSpecConfigEntity createdComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity2); - - commitTransaction(); - - // Verify - assertNotNull(hardwareConfigEntity1_id); - assertNotNull(hardwareConfigEntity2_Id); - assertNotNull(createdComputeSpecConfigEntity1); - assertNotNull(createdComputeSpecConfigEntity2); - - // Execute - // Add hardware configs to compute spec configs - hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity1_id); - hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity2_Id); - hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity2.getId(), hardwareConfigEntity1_id); - - commitTransaction(); - - // Verify - ComputeSpecConfigEntity retrievedComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity1.getId()); - ComputeSpecConfigEntity retrievedComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity2.getId()); - - assertNotNull(retrievedComputeSpecConfigEntity1); - assertNotNull(retrievedComputeSpecConfigEntity2); - assertEquals(2, retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); - assertEquals(1, retrievedComputeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); - assertTrue(retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); - assertTrue(retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity2_Id))); - assertTrue(retrievedComputeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); - } - - @Test - @DirtiesContext - public void testRemoveHardwareConfigEntity() { - // Setup - HardwareConfigEntity hardwareConfigEntity1 = HardwareConfigEntity.fromPojo(hardwareConfig1); - HardwareConfigEntity hardwareConfigEntity2 = HardwareConfigEntity.fromPojo(hardwareConfig2); - ComputeSpecConfigEntity computeSpecConfigEntity1 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig1); - ComputeSpecConfigEntity computeSpecConfigEntity2 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig2); - - // Commit hardware configs - Long hardwareConfigEntity1_id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity1); - Long hardwareConfigEntity2_Id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity2); - - ComputeSpecConfigEntity createdComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity1); - ComputeSpecConfigEntity createdComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity2); - - commitTransaction(); - - // Verify - assertNotNull(hardwareConfigEntity1_id); - assertNotNull(hardwareConfigEntity2_Id); - assertNotNull(createdComputeSpecConfigEntity1); - assertNotNull(createdComputeSpecConfigEntity2); - - // Execute - // Add hardware configs to compute spec configs - hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity1_id); - hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity2_Id); - hibernateComputeSpecConfigEntityService.addHardwareConfigEntity(createdComputeSpecConfigEntity2.getId(), hardwareConfigEntity1_id); - - commitTransaction(); - - // Verify - ComputeSpecConfigEntity retrievedComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity1.getId()); - ComputeSpecConfigEntity retrievedComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity2.getId()); - - assertNotNull(retrievedComputeSpecConfigEntity1); - assertNotNull(retrievedComputeSpecConfigEntity2); - assertEquals(2, retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); - assertEquals(1, retrievedComputeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); - assertTrue(retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); - assertTrue(retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity2_Id))); - assertTrue(retrievedComputeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); - - // Remove hardware configs from compute spec configs - hibernateComputeSpecConfigEntityService.removeHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity1_id); - hibernateComputeSpecConfigEntityService.removeHardwareConfigEntity(createdComputeSpecConfigEntity1.getId(), hardwareConfigEntity2_Id); - - commitTransaction(); - - hibernateComputeSpecConfigEntityService.removeHardwareConfigEntity(createdComputeSpecConfigEntity2.getId(), hardwareConfigEntity1_id); - - commitTransaction(); - - // Verify - retrievedComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity1.getId()); - retrievedComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.retrieve(createdComputeSpecConfigEntity2.getId()); - - assertNotNull(retrievedComputeSpecConfigEntity1); - assertNotNull(retrievedComputeSpecConfigEntity2); - assertEquals(0, retrievedComputeSpecConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); - assertEquals(0, retrievedComputeSpecConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); - } - - @Test - @DirtiesContext - public void testFindByType() { - // Setup - ComputeSpecConfigEntity computeSpecConfigEntity1 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig1); - ComputeSpecConfigEntity computeSpecConfigEntity2 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig2); - ComputeSpecConfigEntity computeSpecConfigEntity3 = ComputeSpecConfigEntity.fromPojo(computeSpecConfig3); - - // Commit compute spec configs - ComputeSpecConfigEntity createdComputeSpecConfigEntity1 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity1); - ComputeSpecConfigEntity createdComputeSpecConfigEntity2 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity2); - ComputeSpecConfigEntity createdComputeSpecConfigEntity3 = hibernateComputeSpecConfigEntityService.create(computeSpecConfigEntity3); - - commitTransaction(); - - // Verify - assertNotNull(createdComputeSpecConfigEntity1); - assertNotNull(createdComputeSpecConfigEntity2); - assertNotNull(createdComputeSpecConfigEntity3); - - // Execute - List retrievedComputeSpecConfigEntities = hibernateComputeSpecConfigEntityService.findByType(JUPYTERHUB); - - // Verify - assertNotNull(retrievedComputeSpecConfigEntities); - assertEquals(2, retrievedComputeSpecConfigEntities.size()); - assertEquals(createdComputeSpecConfigEntity1.getId(), retrievedComputeSpecConfigEntities.get(0).getId()); - assertEquals(createdComputeSpecConfigEntity3.getId(), retrievedComputeSpecConfigEntities.get(1).getId()); - - // Execute - retrievedComputeSpecConfigEntities = hibernateComputeSpecConfigEntityService.findByType(CONTAINER_SERVICE); - - // Verify - assertNotNull(retrievedComputeSpecConfigEntities); - assertEquals(2, retrievedComputeSpecConfigEntities.size()); - assertEquals(createdComputeSpecConfigEntity2.getId(), retrievedComputeSpecConfigEntities.get(0).getId()); - assertEquals(createdComputeSpecConfigEntity3.getId(), retrievedComputeSpecConfigEntities.get(1).getId()); - } - - public void createDummyConfigsAndEntities() { - // Setup hardware - Hardware hardware1 = Hardware.builder() - .name("Small") - .cpuReservation(2.0) - .cpuLimit(4.0) - .memoryReservation("4G") - .memoryLimit("8G") - .build(); - - // Setup hardware constraints - Constraint hardwareConstraint1 = Constraint.builder() - .key("node.role") - .operator(Constraint.Operator.IN) - .values(new HashSet<>(Collections.singletonList("worker"))) - .build(); - - Constraint hardwareConstraint2 = Constraint.builder() - .key("node.instance.type") - .operator(Constraint.Operator.NOT_IN) - .values(new HashSet<>(Arrays.asList("spot", "demand"))) - .build(); - - hardware1.setConstraints(Arrays.asList(hardwareConstraint1, hardwareConstraint2)); - - // Setup hardware environment variables - EnvironmentVariable hardwareEnvironmentVariable1 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); - EnvironmentVariable hardwareEnvironmentVariable2 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); - - hardware1.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable1, hardwareEnvironmentVariable2)); - - // Setup hardware generic resources - GenericResource hardwareGenericResource1 = new GenericResource("nvidia.com/gpu", "2"); - GenericResource hardwareGenericResource2 = new GenericResource("fpga.com/fpga", "1"); - - hardware1.setGenericResources(Arrays.asList(hardwareGenericResource1, hardwareGenericResource2)); - - // Setup hardware scopes - HardwareScope hardwareSiteScope1 = HardwareScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope hardwareProjectScope1 = HardwareScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope userHardwareScope1 = HardwareScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map hardwareScopes1 = new HashMap<>(); - hardwareScopes1.put(Site, hardwareSiteScope1); - hardwareScopes1.put(Project, hardwareProjectScope1); - hardwareScopes1.put(User, userHardwareScope1); - - // Setup a hardware config entity - hardwareConfig1 = HardwareConfig.builder() - .hardware(hardware1) - .scopes(hardwareScopes1) - .build(); - - // Setup second hardware config - Hardware hardware2 = Hardware.builder() - .name("Medium") - .cpuReservation(4.0) - .cpuLimit(4.0) - .memoryReservation("8G") - .memoryLimit("8G") - .build(); - - // Setup hardware constraints - Constraint hardwareConstraint3 = Constraint.builder() - .key("node.role") - .operator(Constraint.Operator.IN) - .values(new HashSet<>(Collections.singletonList("worker"))) - .build(); - - Constraint hardwareConstraint4 = Constraint.builder() - .key("node.instance.type") - .operator(Constraint.Operator.NOT_IN) - .values(new HashSet<>(Arrays.asList("spot", "demand"))) - .build(); - - hardware2.setConstraints(Arrays.asList(hardwareConstraint3, hardwareConstraint4)); - - // Setup hardware environment variables - EnvironmentVariable hardwareEnvironmentVariable3 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); - EnvironmentVariable hardwareEnvironmentVariable4 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); - - hardware2.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable3, hardwareEnvironmentVariable4)); - - // Setup hardware generic resources - GenericResource hardwareGenericResource3 = new GenericResource("nvidia.com/gpu", "2"); - GenericResource hardwareGenericResource4 = new GenericResource("fpga.com/fpga", "1"); - - hardware2.setGenericResources(Arrays.asList(hardwareGenericResource3, hardwareGenericResource4)); - - // Setup hardware scopes - HardwareScope hardwareSiteScope2 = HardwareScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope hardwareProjectScope2 = HardwareScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope userHardwareScope2 = HardwareScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map hardwareScopes2 = new HashMap<>(); - hardwareScopes2.put(Site, hardwareSiteScope2); - hardwareScopes2.put(Project, hardwareProjectScope2); - hardwareScopes2.put(User, userHardwareScope2); - - // Setup second hardware config entity - hardwareConfig2 = HardwareConfig.builder() - .hardware(hardware2) - .scopes(hardwareScopes2) - .build(); - - // Setup first compute spec - // Create mount first - Mount mount1 = Mount.builder() - .localPath("/home/jovyan/work") - .containerPath("/home/jovyan/work") - .readOnly(false) - .build(); - - Mount mount2 = Mount.builder() - .localPath("/tools/MATLAB/R2019b") - .containerPath("/tools/MATLAB/R2019b") - .readOnly(true) - .build(); - - ComputeSpec computeSpec1 = ComputeSpec.builder() - .name("Jupyter Datascience Notebook") - .image("jupyter/datascience-notebook:hub-3.0.0") - .environmentVariables(Collections.singletonList(new EnvironmentVariable("JUPYTER_ENABLE_LAB", "yes"))) - .mounts(Arrays.asList(mount1, mount2)) - .build(); - - ComputeSpecScope computeSpecSiteScope1 = ComputeSpecScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeSpecScope computeSpecProjectScope1 = ComputeSpecScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeSpecScope computeSpecUserScope1 = ComputeSpecScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map computeSpecScopes1 = new HashMap<>(); - computeSpecScopes1.put(Site, computeSpecSiteScope1); - computeSpecScopes1.put(Project, computeSpecProjectScope1); - computeSpecScopes1.put(User, computeSpecUserScope1); - - ComputeSpecHardwareOptions computeSpecHardwareOptions1 = ComputeSpecHardwareOptions.builder() - .allowAllHardware(true) - .hardwareConfigs(new HashSet<>()) - .build(); - - computeSpecConfig1 = ComputeSpecConfig.builder() - .configTypes(new HashSet<>(Collections.singletonList(JUPYTERHUB))) - .computeSpec(computeSpec1) - .scopes(computeSpecScopes1) - .hardwareOptions(computeSpecHardwareOptions1) - .build(); - - // Setup second compute spec - ComputeSpec computeSpec2 = ComputeSpec.builder() - .name("XNAT Datascience Notebook") - .image("xnat/datascience-notebook:latest") - .environmentVariables(new ArrayList<>()) - .mounts(new ArrayList<>()) - .build(); - - ComputeSpecScope computeSpecSiteScope2 = ComputeSpecScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeSpecScope computeSpecProjectScope2 = ComputeSpecScope.builder() - .scope(Project) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("Project1"))) - .build(); - - ComputeSpecScope computeSpecUserScope2 = ComputeSpecScope.builder() - .scope(User) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("User1"))) - .build(); - - Map computeSpecScopes2 = new HashMap<>(); - computeSpecScopes2.put(Site, computeSpecSiteScope2); - computeSpecScopes2.put(Project, computeSpecProjectScope2); - computeSpecScopes2.put(User, computeSpecUserScope2); - - ComputeSpecHardwareOptions computeSpecHardwareOptions2 = ComputeSpecHardwareOptions.builder() - .allowAllHardware(false) - .hardwareConfigs(new HashSet<>()) - .build(); - - computeSpecConfig2 = ComputeSpecConfig.builder() - .configTypes(new HashSet<>(Collections.singletonList(CONTAINER_SERVICE))) - .computeSpec(computeSpec2) - .scopes(computeSpecScopes2) - .hardwareOptions(computeSpecHardwareOptions2) - .build(); - - // Setup third compute spec - ComputeSpec computeSpec3 = ComputeSpec.builder() - .name("MATLAB Datascience Notebook") - .image("matlab/datascience-notebook:latest") - .environmentVariables(new ArrayList<>()) - .mounts(new ArrayList<>()) - .build(); - - ComputeSpecScope computeSpecSiteScope3 = ComputeSpecScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeSpecScope computeSpecProjectScope3 = ComputeSpecScope.builder() - .scope(Project) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("Project1"))) - .build(); - - ComputeSpecScope computeSpecUserScope3 = ComputeSpecScope.builder() - .scope(User) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("User1"))) - .build(); - - Map computeSpecScopes3 = new HashMap<>(); - computeSpecScopes2.put(Site, computeSpecSiteScope3); - computeSpecScopes2.put(Project, computeSpecProjectScope3); - computeSpecScopes2.put(User, computeSpecUserScope3); - - ComputeSpecHardwareOptions computeSpecHardwareOptions3 = ComputeSpecHardwareOptions.builder() - .allowAllHardware(false) - .hardwareConfigs(new HashSet<>()) - .build(); - - computeSpecConfig3 = ComputeSpecConfig.builder() - .configTypes(new HashSet<>(Arrays.asList(CONTAINER_SERVICE, JUPYTERHUB))) - .computeSpec(computeSpec3) - .scopes(computeSpecScopes3) - .hardwareOptions(computeSpecHardwareOptions3) - .build(); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java index 1ae2e38..fe43310 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubEnvironmentsAndHardwareInitializerTestConfig.java @@ -1,6 +1,6 @@ package org.nrg.xnatx.plugins.jupyterhub.config; -import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnatx.plugins.jupyterhub.initialize.JupyterHubEnvironmentsAndHardwareInitializer; @@ -16,12 +16,12 @@ public class JupyterHubEnvironmentsAndHardwareInitializerTestConfig { @Bean public JupyterHubEnvironmentsAndHardwareInitializer JupyterHubJobTemplateInitializer(final XFTManagerHelper mockXFTManagerHelper, final XnatAppInfo mockXnatAppInfo, - final ComputeSpecConfigService mockComputeSpecConfigService, + final ComputeEnvironmentConfigService mockComputeEnvironmentConfigService, final HardwareConfigService mockHardwareConfigService) { return new JupyterHubEnvironmentsAndHardwareInitializer( mockXFTManagerHelper, mockXnatAppInfo, - mockComputeSpecConfigService, + mockComputeEnvironmentConfigService, mockHardwareConfigService ); } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java index 282aebb..6c73b87 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java @@ -5,7 +5,7 @@ import org.nrg.framework.services.NrgEventServiceI; import org.nrg.framework.services.SerializerService; import org.nrg.framework.utilities.OrderedProperties; -import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.prefs.services.NrgPreferenceService; @@ -145,8 +145,8 @@ public XnatAppInfo mockXnatAppInfo() { } @Bean - public ComputeSpecConfigService mockComputeSpecConfigService() { - return Mockito.mock(ComputeSpecConfigService.class); + public ComputeEnvironmentConfigService mockComputeEnvironmentConfigService() { + return Mockito.mock(ComputeEnvironmentConfigService.class); } @Bean diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java index 22f2e15..48c66cb 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubEnvironmentsAndHardwareInitializerTest.java @@ -4,9 +4,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.nrg.xnat.compute.models.ComputeSpecConfig; +import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; import org.nrg.xnat.compute.models.HardwareConfig; -import org.nrg.xnat.compute.services.ComputeSpecConfigService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xnat.initialization.tasks.InitializingTaskException; import org.nrg.xnat.services.XnatAppInfo; @@ -32,7 +32,7 @@ public class JupyterHubEnvironmentsAndHardwareInitializerTest { @Autowired private JupyterHubEnvironmentsAndHardwareInitializer jupyterHubJobTemplateInitializer; @Autowired private XFTManagerHelper mockXFTManagerHelper; @Autowired private XnatAppInfo mockXnatAppInfo; - @Autowired private ComputeSpecConfigService mockComputeSpecConfigService; + @Autowired private ComputeEnvironmentConfigService mockComputeEnvironmentConfigService; @Autowired private HardwareConfigService mockHardwareConfigService; @After @@ -40,7 +40,7 @@ public void after() { Mockito.reset( mockXFTManagerHelper, mockXnatAppInfo, - mockComputeSpecConfigService, + mockComputeEnvironmentConfigService, mockHardwareConfigService ); } @@ -83,8 +83,8 @@ public void testCallImpl_ConfigsAlreadyExist() { // Setup mock - when(mockComputeSpecConfigService.getByType(any())) - .thenReturn(Collections.singletonList(new ComputeSpecConfig())); + when(mockComputeEnvironmentConfigService.getByType(any())) + .thenReturn(Collections.singletonList(new ComputeEnvironmentConfig())); when(mockHardwareConfigService.retrieveAll()) .thenReturn(Collections.singletonList(new HardwareConfig())); @@ -96,7 +96,7 @@ public void testCallImpl_ConfigsAlreadyExist() { } // Verify - verify(mockComputeSpecConfigService, never()).create(any()); + verify(mockComputeEnvironmentConfigService, never()).create(any()); verify(mockHardwareConfigService, never()).create(any()); } @@ -107,7 +107,7 @@ public void testCallImpl() { when(mockXnatAppInfo.isInitialized()).thenReturn(true); // Setup mock - when(mockComputeSpecConfigService.getByType(any())) + when(mockComputeEnvironmentConfigService.getByType(any())) .thenReturn(Collections.emptyList()); when(mockHardwareConfigService.retrieveAll()) .thenReturn(Collections.emptyList()); @@ -120,7 +120,7 @@ public void testCallImpl() { } // Verify - verify(mockComputeSpecConfigService, atLeast(1)).create(any()); + verify(mockComputeEnvironmentConfigService, atLeast(1)).create(any()); verify(mockHardwareConfigService, atLeast(1)).create(any()); } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java index 0173880..511d3b6 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java @@ -160,7 +160,7 @@ public void before() throws Exception { .itemLabel("TestProject") .projectId("TestProject") .eventTrackingId("20220822T201541799Z") - .computeSpecConfigId(1L) + .computeEnvironmentConfigId(1L) .hardwareConfigId(1L) .build(); diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java index 9ba9105..63e936f 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java @@ -67,7 +67,7 @@ public class DefaultJupyterHubServiceTest { private final String servername = ""; private final String eventTrackingId = "eventTrackingId"; private final String projectId = "TestProject"; - private final Long computeSpecConfigId = 3L; + private final Long computeEnvironmentConfigId = 3L; private final Long hardwareConfigId = 2L; private User userWithServers; private User userNoServers; @@ -108,7 +108,7 @@ public void before() throws org.nrg.xdat.security.user.exceptions.UserNotFoundEx .itemLabel(projectId) .projectId(projectId) .eventTrackingId(eventTrackingId) - .computeSpecConfigId(computeSpecConfigId) + .computeEnvironmentConfigId(computeEnvironmentConfigId) .hardwareConfigId(hardwareConfigId) .build(); @@ -121,7 +121,7 @@ public void before() throws org.nrg.xdat.security.user.exceptions.UserNotFoundEx .itemLabel("Subject1") .projectId(projectId) .eventTrackingId(eventTrackingId) - .computeSpecConfigId(computeSpecConfigId) + .computeEnvironmentConfigId(computeEnvironmentConfigId) .hardwareConfigId(hardwareConfigId) .build(); @@ -134,7 +134,7 @@ public void before() throws org.nrg.xdat.security.user.exceptions.UserNotFoundEx .itemLabel("Experiment1") .projectId(projectId) .eventTrackingId(eventTrackingId) - .computeSpecConfigId(computeSpecConfigId) + .computeEnvironmentConfigId(computeEnvironmentConfigId) .hardwareConfigId(hardwareConfigId) .build(); @@ -147,7 +147,7 @@ public void before() throws org.nrg.xdat.security.user.exceptions.UserNotFoundEx .itemLabel("Scan1") .projectId(projectId) .eventTrackingId(eventTrackingId) - .computeSpecConfigId(computeSpecConfigId) + .computeEnvironmentConfigId(computeEnvironmentConfigId) .hardwareConfigId(hardwareConfigId) .build(); @@ -412,7 +412,7 @@ public void testStartServer_Timeout() throws UserNotFoundException, ResourceAlre // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), - eq(projectId), eq(projectId), eq(computeSpecConfigId), + eq(projectId), eq(projectId), eq(computeEnvironmentConfigId), eq(hardwareConfigId), eq(eventTrackingId)); // Verify JupyterHub start server request sent @@ -447,7 +447,7 @@ public void testStartServer_Success() throws Exception { // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), - eq(projectId), eq(projectId), eq(computeSpecConfigId), + eq(projectId), eq(projectId), eq(computeEnvironmentConfigId), eq(hardwareConfigId), eq(eventTrackingId)); // Verify JupyterHub start server request sent @@ -484,7 +484,7 @@ public void testStartServer_CreateUser_Success() throws Exception { // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), - eq(projectId), eq(projectId), eq(computeSpecConfigId), + eq(projectId), eq(projectId), eq(computeEnvironmentConfigId), eq(hardwareConfigId), eq(eventTrackingId)); // Verify JupyterHub start server request sent diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java index 5909f9d..92525d8 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsServiceTest.java @@ -114,11 +114,11 @@ public void testToTaskTemplate() { .readOnly(false) .build(); - // Setup first compute spec - ComputeSpec computeSpec1 = ComputeSpec.builder() + // Setup first compute environment + ComputeEnvironment computeEnvironment1 = ComputeEnvironment.builder() .name("Jupyter Datascience Notebook") .image("jupyter/datascience-notebook:hub-3.0.0") - .environmentVariables(Collections.singletonList(new EnvironmentVariable("COMPUTE_SPEC", "1"))) + .environmentVariables(Collections.singletonList(new EnvironmentVariable("COMPUTE_ENV", "1"))) .mounts(Arrays.asList(mount1, mount2)) .build(); @@ -142,7 +142,7 @@ public void testToTaskTemplate() { // Now create the job template JobTemplate template = JobTemplate.builder() - .computeSpec(computeSpec1) + .computeEnvironment(computeEnvironment1) .hardware(hardware1) .constraints(Arrays.asList(constraint1, constraint2, constraint3)) .build(); @@ -151,16 +151,16 @@ public void testToTaskTemplate() { TaskTemplate result = userOptionsService.toTaskTemplate(template); // Verify the results - assertEquals(computeSpec1.getImage(), result.getContainerSpec().getImage()); + assertEquals(computeEnvironment1.getImage(), result.getContainerSpec().getImage()); - // Verify the environment variables from the hardware and compute spec are merged + // Verify the environment variables from the hardware and compute environment are merged Map expectedEnv = new HashMap<>(); - expectedEnv.put("COMPUTE_SPEC", "1"); + expectedEnv.put("COMPUTE_ENV", "1"); expectedEnv.put("MATLAB_LICENSE_FILE", "12345@myserver"); expectedEnv.put("NVIDIA_VISIBLE_DEVICES", "all"); assertEquals(expectedEnv, result.getContainerSpec().getEnv()); - // Verify the mounts from the hardware and compute spec are merged + // Verify the mounts from the hardware and compute environment are merged assertEquals(2, result.getContainerSpec().getMounts().size()); assertEquals("/data/xnat/archive/Project1", result.getContainerSpec().getMounts().get(0).getSource()); assertEquals("/data/xnat/archive/Project1", result.getContainerSpec().getMounts().get(0).getTarget()); From 0e015e167d59ce5484d1d3a66465873a93bcd845 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 26 Jun 2023 18:18:30 -0500 Subject: [PATCH 25/49] JHP-64: More flexibility with isAvailable/getAvailable interfaces --- .../nrg/xnat/compute/models/AccessScope.java | 49 +++++ .../models/ComputeEnvironmentScope.java | 6 +- .../xnat/compute/models/ConstraintScope.java | 6 +- .../xnat/compute/models/HardwareScope.java | 6 +- .../rest/ComputeEnvironmentConfigsApi.java | 28 ++- .../ComputeEnvironmentConfigService.java | 8 +- .../services/ConstraintConfigService.java | 4 +- .../services/HardwareConfigService.java | 4 +- .../compute/services/JobTemplateService.java | 7 +- ...efaultComputeEnvironmentConfigService.java | 112 +++++------ .../impl/DefaultConstraintConfigService.java | 42 ++-- .../impl/DefaultHardwareConfigService.java | 37 ++-- .../impl/DefaultJobTemplateService.java | 44 +++-- .../impl/DefaultJupyterHubService.java | 6 +- .../impl/DefaultUserOptionsService.java | 15 +- .../compute/compute-environment-configs.js | 24 ++- .../xnat/plugin/compute/hardware-configs.js | 1 - .../plugin/jupyterhub/jupyterhub-servers.js | 8 +- .../ComputeEnvironmentConfigsApiTest.java | 13 +- ...ltComputeEnvironmentConfigServiceTest.java | 184 +++++++++++++++--- .../DefaultConstraintConfigServiceTest.java | 10 +- .../DefaultHardwareConfigServiceTest.java | 21 +- .../impl/DefaultJobTemplateServiceTest.java | 87 ++++++--- .../impl/DefaultJupyterHubServiceTest.java | 8 +- 24 files changed, 510 insertions(+), 220 deletions(-) create mode 100644 src/main/java/org/nrg/xnat/compute/models/AccessScope.java diff --git a/src/main/java/org/nrg/xnat/compute/models/AccessScope.java b/src/main/java/org/nrg/xnat/compute/models/AccessScope.java new file mode 100644 index 0000000..09d5405 --- /dev/null +++ b/src/main/java/org/nrg/xnat/compute/models/AccessScope.java @@ -0,0 +1,49 @@ +package org.nrg.xnat.compute.models; + +import org.nrg.framework.constants.Scope; + +import java.util.Map; +import java.util.Set; + +public interface AccessScope { + + Scope getScope(); + + boolean isEnabled(); // is enabled for all ids + + Set getIds(); // ids that are enabled for this scope (if isEnabled is false) + + /** + * A scope/id combination is enabled if the scope is enabled for all ids or if the id is in the set of ids + * + * @param id The id to check + * @return Whether the scope/id combination is enabled + */ + default boolean isEnabledFor(String id) { + if (isEnabled()) { + return true; + } else { + return getIds().contains(id); + } + } + + /** + * Given the provided user execution scope, determine if the config is enabled for + * + * @param configRequiredAccessScopes The required access scopes for the config + * @param userExecutionScope The user's execution scope + * @return Whether the config is enabled for the provided execution scope + */ + static boolean isEnabledFor(Map configRequiredAccessScopes, Map userExecutionScope) { + for (Scope scope : Scope.values()) { + if (configRequiredAccessScopes.containsKey(scope)) { + AccessScope configRequiredAccessScope = configRequiredAccessScopes.get(scope); + if (!configRequiredAccessScope.isEnabledFor(userExecutionScope.get(scope))) { + return false; + } + } + } + return true; + } + +} diff --git a/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentScope.java b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentScope.java index b71792e..a1833ef 100644 --- a/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentScope.java +++ b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentScope.java @@ -12,10 +12,10 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class ComputeEnvironmentScope { +public class ComputeEnvironmentScope implements AccessScope { private Scope scope; - private boolean enabled; // For project and user scopes, this is whether the scope is enabled for all projects or users - private Set ids; // For project and user scopes, this is the set of project or user IDs that are enabled for this scope (if isEnabled is false) + private boolean enabled; // Whether the scope is enabled for all ids or only for the ids in the set + private Set ids; // The set of ids that are enabled for this scope (if isEnabled is false) } diff --git a/src/main/java/org/nrg/xnat/compute/models/ConstraintScope.java b/src/main/java/org/nrg/xnat/compute/models/ConstraintScope.java index ed5cccb..fc9678f 100644 --- a/src/main/java/org/nrg/xnat/compute/models/ConstraintScope.java +++ b/src/main/java/org/nrg/xnat/compute/models/ConstraintScope.java @@ -12,10 +12,10 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class ConstraintScope { +public class ConstraintScope implements AccessScope { private Scope scope; - private boolean enabled; // For project and user scopes, this is whether the scope is enabled for all projects or users - private Set ids; // For project and user scopes, this is the set of project or user IDs that are enabled for this scope (if isEnabled is false) + private boolean enabled; // Whether the scope is enabled for all ids or only for the ids in the set + private Set ids; // The set of ids that are enabled for this scope (if isEnabled is false) } diff --git a/src/main/java/org/nrg/xnat/compute/models/HardwareScope.java b/src/main/java/org/nrg/xnat/compute/models/HardwareScope.java index e583c69..288c767 100644 --- a/src/main/java/org/nrg/xnat/compute/models/HardwareScope.java +++ b/src/main/java/org/nrg/xnat/compute/models/HardwareScope.java @@ -12,10 +12,10 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class HardwareScope { +public class HardwareScope implements AccessScope { private Scope scope; - private boolean enabled; // For project and user scopes, this is whether the scope is enabled for all projects or users - private Set ids; // For project and user scopes, this is the set of project or user IDs that are enabled for this scope (if isEnabled is false) + private boolean enabled; // Whether the scope is enabled for all ids or only for the ids in the set + private Set ids; // The set of ids that are enabled for this scope (if isEnabled is false) } diff --git a/src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java b/src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java index e1528da..2f567b1 100644 --- a/src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java +++ b/src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java @@ -5,6 +5,7 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import org.nrg.framework.annotations.XapiRestController; +import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; import org.nrg.xapi.rest.AbstractXapiRestController; import org.nrg.xapi.rest.XapiRequestMapping; @@ -17,6 +18,8 @@ import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static org.nrg.xdat.security.helpers.AccessLevel.Admin; import static org.nrg.xdat.security.helpers.AccessLevel.Read; @@ -75,7 +78,7 @@ public ComputeEnvironmentConfig get(@PathVariable("id") final Long id) throws No @ApiResponse(code = 500, message = "Unexpected error") }) @ResponseStatus(HttpStatus.CREATED) - @XapiRequestMapping(value = "",consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.POST, restrictTo = Admin) + @XapiRequestMapping(value = "", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.POST, restrictTo = Admin) public ComputeEnvironmentConfig create(@RequestBody final ComputeEnvironmentConfig computeEnvironmentConfig) { return computeEnvironmentConfigService.create(computeEnvironmentConfig); } @@ -88,7 +91,7 @@ public ComputeEnvironmentConfig create(@RequestBody final ComputeEnvironmentConf @ApiResponse(code = 404, message = "Compute environment config not found."), @ApiResponse(code = 500, message = "Unexpected error") }) - @XapiRequestMapping(value = "/{id}",consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.PUT, restrictTo = Admin) + @XapiRequestMapping(value = "/{id}", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.PUT, restrictTo = Admin) public ComputeEnvironmentConfig update(@PathVariable("id") final Long id, @RequestBody final ComputeEnvironmentConfig computeEnvironmentConfig) throws NotFoundException { if (!id.equals(computeEnvironmentConfig.getId())) { @@ -112,7 +115,7 @@ public void delete(@PathVariable("id") final Long id) throws NotFoundException { computeEnvironmentConfigService.delete(id); } - @ApiOperation(value = "Get all available compute environment configs for the given user and project.", response = ComputeEnvironmentConfig.class, responseContainer = "List") + @ApiOperation(value = "Get all available compute environment configs for the provided execution scope.", response = ComputeEnvironmentConfig.class, responseContainer = "List") @ApiResponses({ @ApiResponse(code = 200, message = "Compute environment configs successfully retrieved."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @@ -120,10 +123,21 @@ public void delete(@PathVariable("id") final Long id) throws NotFoundException { @ApiResponse(code = 500, message = "Unexpected error") }) @XapiRequestMapping(value = "/available", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Read) - public List getAvailable(@RequestParam(value = "user") final String user, - @RequestParam(value = "project") final String project, - @RequestParam(value = "type", required = false) final ComputeEnvironmentConfig.ConfigType type) { - return computeEnvironmentConfigService.getAvailable(user, project, type); + public List getAvailable(@RequestParam final Map params) { + ComputeEnvironmentConfig.ConfigType type = null; + + // If the type is specified, remove it from the params map so it doesn't get used as an execution scope. + if (params.containsKey("type")) { + type = ComputeEnvironmentConfig.ConfigType.valueOf(params.get("type")); + params.remove("type"); + } + + // Get the execution scope from the params map. + Map executionScope = params.entrySet().stream() + .filter(entry -> Scope.getCodes().contains(entry.getKey())) + .collect(Collectors.toMap(entry -> Scope.getScope(entry.getKey()), Map.Entry::getValue)); + + return computeEnvironmentConfigService.getAvailable(type, executionScope); } } diff --git a/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java b/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java index 6c944e9..829b8eb 100644 --- a/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java @@ -1,9 +1,11 @@ package org.nrg.xnat.compute.services; +import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; import java.util.List; +import java.util.Map; import java.util.Optional; public interface ComputeEnvironmentConfigService { @@ -12,9 +14,9 @@ public interface ComputeEnvironmentConfigService { Optional retrieve(Long id); List getAll(); List getByType(ComputeEnvironmentConfig.ConfigType type); - List getAvailable(String user, String project); - List getAvailable(String user, String project, ComputeEnvironmentConfig.ConfigType type); - boolean isAvailable(String user, String project, Long id); + List getAvailable(Map executionScope); + List getAvailable(ComputeEnvironmentConfig.ConfigType type, Map executionScope); + boolean isAvailable(Long id, Map executionScope); ComputeEnvironmentConfig create(ComputeEnvironmentConfig computeEnvironmentConfig); ComputeEnvironmentConfig update(ComputeEnvironmentConfig computeEnvironmentConfig) throws NotFoundException; void delete(Long id) throws NotFoundException; diff --git a/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigService.java b/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigService.java index bbb2648..fe0c8f2 100644 --- a/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigService.java @@ -1,16 +1,18 @@ package org.nrg.xnat.compute.services; +import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; import org.nrg.xnat.compute.models.ConstraintConfig; import java.util.List; +import java.util.Map; import java.util.Optional; public interface ConstraintConfigService { Optional retrieve(Long id); List getAll(); - List getAvailable(String project); + List getAvailable(Map executionScope); ConstraintConfig create(ConstraintConfig config); ConstraintConfig update(ConstraintConfig config) throws NotFoundException; void delete(Long id); diff --git a/src/main/java/org/nrg/xnat/compute/services/HardwareConfigService.java b/src/main/java/org/nrg/xnat/compute/services/HardwareConfigService.java index 3a1d2a9..64b0b9a 100644 --- a/src/main/java/org/nrg/xnat/compute/services/HardwareConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/HardwareConfigService.java @@ -1,9 +1,11 @@ package org.nrg.xnat.compute.services; +import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; import org.nrg.xnat.compute.models.HardwareConfig; import java.util.List; +import java.util.Map; import java.util.Optional; public interface HardwareConfigService { @@ -14,6 +16,6 @@ public interface HardwareConfigService { HardwareConfig create(HardwareConfig hardwareConfig); HardwareConfig update(HardwareConfig hardwareConfig) throws NotFoundException; void delete(Long id) throws NotFoundException; - boolean isAvailable(String user, String project, Long id); + boolean isAvailable(Long id, Map executionScope); } diff --git a/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java b/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java index bd3b089..9e1f3da 100644 --- a/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java +++ b/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java @@ -1,10 +1,13 @@ package org.nrg.xnat.compute.services; +import org.nrg.framework.constants.Scope; import org.nrg.xnat.compute.models.JobTemplate; +import java.util.Map; + public interface JobTemplateService { - boolean isAvailable(String user, String project, Long computeEnvironmentConfigId, Long hardwareConfigId); - JobTemplate resolve(String user, String project, Long computeEnvironmentConfigId, Long hardwareConfigId); + boolean isAvailable(Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope); + JobTemplate resolve(Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope); } diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java index 94de0f4..048ce91 100644 --- a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java @@ -32,6 +32,7 @@ public DefaultComputeEnvironmentConfigService(final ComputeEnvironmentConfigEnti /** * Checks if a ComputeEnvironmentConfig with the given ID exists. + * * @param id The ID of the ComputeEnvironmentConfig to check for. * @return True if a ComputeEnvironmentConfig with the given ID exists, false otherwise. */ @@ -42,6 +43,7 @@ public boolean exists(Long id) { /** * Gets a ComputeEnvironmentConfig by its ID. + * * @param id The ID of the ComputeEnvironmentConfig to retrieve. * @return The ComputeEnvironmentConfig with the given ID, if it exists or else an empty Optional. */ @@ -54,6 +56,7 @@ public Optional retrieve(Long id) { /** * Get all ComputeEnvironmentConfigs. + * * @return A list of all ComputeEnvironmentConfigs. */ @Override @@ -67,6 +70,7 @@ public List getAll() { /** * Creates a new ComputeEnvironmentConfig. + * * @param type The type of the ComputeEnvironmentConfig to create. * @return The newly created ComputeEnvironmentConfig. */ @@ -80,25 +84,31 @@ public List getByType(ComputeEnvironmentConfig.ConfigT } /** - * Get all ComputeEnvironmentConfigs that are available to the given user and project regardless of type. - * @param user The user to check for. - * @param project The project to check for. - * @return A list of all ComputeEnvironmentConfigs that are available to the given user and project. + * Get all ComputeEnvironmentConfigs that are available to the provided execution scope. A config is available + * provided the execution scope contains all the required access scopes of the config and each scope/id combination + * is enabled. + * + * @param executionScope The execution scope to check availability for. + * Example {Scope.Site: "", Scope.Project: "Project1", Scope.User: "User1"} + * @return A list of ComputeEnvironmentConfigs that are available for the provided execution scope. */ @Override - public List getAvailable(String user, String project) { - return getAvailable(user, project, null); + public List getAvailable(Map executionScope) { + return getAvailable(null, executionScope); } /** - * Get all ComputeEnvironmentConfigs of the given type that are available to the given user and project. - * @param user The user to check for. - * @param project The project to check for. - * @param type The type of ComputeEnvironmentConfig to check for. - * @return A list of all ComputeEnvironmentConfigs of the given type that are available to the given user and project. + * Get the ComputeEnvironmentConfigs of the given type that are available to the provided execution scope. A config + * is available provided the execution scope contains all the required access scopes of the config and each scope/id + * combination is enabled. + * + * @param type The type of the ComputeEnvironmentConfig to get. If null, all types are returned. + * @param executionScope The execution scope to check availability for. + * Example {Scope.Site: "", Scope.Project: "Project1", Scope.User: "User1", Scope.DataType: "xnat:mrSessionData"} + * @return A list of ComputeEnvironmentConfigs that are available for the provided requestedScope. */ @Override - public List getAvailable(String user, String project, ComputeEnvironmentConfig.ConfigType type) { + public List getAvailable(ComputeEnvironmentConfig.ConfigType type, Map executionScope) { List all; if (type == null) { @@ -108,22 +118,25 @@ public List getAvailable(String user, String project, } final List available = all.stream().filter(computeEnvironmentConfig -> { - final ComputeEnvironmentScope siteScope = computeEnvironmentConfig.getScopes().get(Scope.Site); - final ComputeEnvironmentScope projectScope = computeEnvironmentConfig.getScopes().get(Scope.Project); - final ComputeEnvironmentScope userScope = computeEnvironmentConfig.getScopes().get(Scope.User); + Map requiredScopes = computeEnvironmentConfig.getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); + return AccessScope.isEnabledFor(requiredScopes, executionScope); }).collect(Collectors.toList()); available.forEach(computeEnvironmentConfig -> { + // Filter out any hardware configs that are not available to the execution scope final Set hardwareConfigs = computeEnvironmentConfig.getHardwareOptions().getHardwareConfigs(); final Set availableHardware = hardwareConfigs.stream() .filter(hardwareConfig -> { - final HardwareScope siteScope = hardwareConfig.getScopes().get(Scope.Site); - final HardwareScope projectScope = hardwareConfig.getScopes().get(Scope.Project); - final HardwareScope userScope = hardwareConfig.getScopes().get(Scope.User); + Map requiredScopes = hardwareConfig.getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); + return AccessScope.isEnabledFor(requiredScopes, executionScope); }).collect(Collectors.toSet()); computeEnvironmentConfig.getHardwareOptions().setHardwareConfigs(availableHardware); @@ -133,29 +146,34 @@ public List getAvailable(String user, String project, } /** - * Checks if the given ComputeEnvironmentConfig is available to the given user and project. - * @param user The user to check for. - * @param project The project to check for. - * @param id The ID of the ComputeEnvironmentConfig to check for. - * @return True if the ComputeEnvironmentConfig with the given ID is available to the given user and project, false otherwise. + * Checks if a ComputeEnvironmentConfig with the given ID is available to the provided scope/id combinations. A + * config is available provided the execution scope contains all the required access scopes of the config and each + * scope/id combination is enabled. + * + * @param id The ID of the ComputeEnvironmentConfig to check availability for. + * @param executionScope The execution scope to check availability for. + * @return True if the ComputeEnvironmentConfig with the given ID is available for the provided execution scope, + * false otherwise. */ @Override - public boolean isAvailable(String user, String project, Long id) { + public boolean isAvailable(Long id, Map executionScope) { final Optional computeEnvironmentConfig = retrieve(id); if (!computeEnvironmentConfig.isPresent()) { return false; } - final ComputeEnvironmentScope siteScope = computeEnvironmentConfig.get().getScopes().get(Scope.Site); - final ComputeEnvironmentScope projectScope = computeEnvironmentConfig.get().getScopes().get(Scope.Project); - final ComputeEnvironmentScope userScope = computeEnvironmentConfig.get().getScopes().get(Scope.User); + Map requiredScopes = computeEnvironmentConfig.get().getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); + return AccessScope.isEnabledFor(requiredScopes, executionScope); } /** * Creates a new ComputeEnvironmentConfig. + * * @param computeEnvironmentConfig The ComputeEnvironmentConfig to create. * @return The newly created ComputeEnvironmentConfig, with its ID set. */ @@ -177,6 +195,7 @@ public ComputeEnvironmentConfig create(ComputeEnvironmentConfig computeEnvironme /** * Updates an existing ComputeEnvironmentConfig. + * * @param computeEnvironmentConfig The ComputeEnvironmentConfig to update. * @return The updated ComputeEnvironmentConfig. * @throws NotFoundException If the ComputeEnvironmentConfig to update does not exist. @@ -204,6 +223,7 @@ public ComputeEnvironmentConfig update(ComputeEnvironmentConfig computeEnvironme /** * Deletes an existing ComputeEnvironmentConfig. + * * @param id The ID of the ComputeEnvironmentConfig to delete. * @throws NotFoundException If the ComputeEnvironmentConfig to delete does not exist. */ @@ -230,38 +250,9 @@ public void delete(Long id) throws NotFoundException { computeEnvironmentConfigEntityService.delete(id); } - /** - * Checks if the scope is enabled for the given user and project. - * @param user The user to check for. - * @param project The project to check for. - * @param siteScope The site scope to check. - * @param userScope The user scope to check. - * @param projectScope The project scope to check. - * @return True if the scopes are enabled for the given user and project, false otherwise. - */ - protected boolean isScopeEnabledForSiteUserAndProject(String user, String project, ComputeEnvironmentScope siteScope, ComputeEnvironmentScope userScope, ComputeEnvironmentScope projectScope) { - return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled - userScope != null && (userScope.isEnabled() || userScope.getIds().contains(user)) && // User scope must be enabled for all users or the user must be in the list - projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list - } - - /** - * Checks if the scope is enabled for the given user and project. - * @param user The user to check for. - * @param project The project to check for. - * @param siteScope The site scope to check. - * @param userScope The user scope to check. - * @param projectScope The project scope to check. - * @return True if the scopes are enabled for the given user and project, false otherwise. - */ - protected boolean isScopeEnabledForSiteUserAndProject(String user, String project, HardwareScope siteScope, HardwareScope userScope, HardwareScope projectScope) { - return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled - userScope != null && (userScope.isEnabled() || userScope.getIds().contains(user)) && // User scope must be enabled for all users or the user must be in the list - projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list - } - /** * Connect the hardware config entities to the compute environment config entity. + * * @param computeEnvironmentConfig The compute environment config to connect the hardware configs to. */ protected void setHardwareConfigsForComputeEnvironmentConfig(ComputeEnvironmentConfig computeEnvironmentConfig) { @@ -308,6 +299,7 @@ protected void setHardwareConfigsForComputeEnvironmentConfig(ComputeEnvironmentC /** * Validates the given ComputeEnvironmentConfig. Throws an IllegalArgumentException if the ComputeEnvironmentConfig is invalid. + * * @param config The ComputeEnvironmentConfig to validate. */ protected void validate(ComputeEnvironmentConfig config) { diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigService.java index 205700e..6afe944 100644 --- a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigService.java @@ -5,6 +5,7 @@ import org.nrg.framework.constants.Scope; import org.nrg.framework.exceptions.NotFoundException; import org.nrg.xnat.compute.entities.ConstraintConfigEntity; +import org.nrg.xnat.compute.models.AccessScope; import org.nrg.xnat.compute.models.Constraint; import org.nrg.xnat.compute.models.ConstraintConfig; import org.nrg.xnat.compute.models.ConstraintScope; @@ -32,6 +33,7 @@ public DefaultConstraintConfigService(ConstraintConfigEntityService constraintCo /** * Returns the constraint config with the given id. + * * @param id The id of the constraint config to retrieve * @return The constraint config with the given id */ @@ -44,6 +46,7 @@ public Optional retrieve(Long id) { /** * Get all constraint configs. + * * @return List of all constraint configs */ @Override @@ -56,22 +59,32 @@ public List getAll() { } /** - * Returns all constraint configs that are available for the given project. - * @param project The project to get constraint configs for - * @return All constraint configs that are available for the given project + * Returns all the constraint configs that are available to the provided execution scope. + * + * @param executionScope The scope to get constraint configs for + * (e.g. {Scope.Project: "project1", Scope.User: "user1"}) + * @return All constraint configs that are available for the given scope */ @Override - public List getAvailable(String project) { + public List getAvailable(Map executionScope) { return constraintConfigEntityService .getAll() .stream() .map(ConstraintConfigEntity::toPojo) - .filter(config -> isScopeEnabledForSiteAndProject(project, config.getScopes().get(Scope.Site), config.getScopes().get(Scope.Project))) + .filter(config -> { + Map requiredScopes = config.getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + return AccessScope.isEnabledFor(requiredScopes, executionScope); + }) .collect(Collectors.toList()); } /** * Creates a new constraint config. + * * @param config The constraint config to create * @return The created constraint config */ @@ -87,6 +100,7 @@ public ConstraintConfig create(ConstraintConfig config) { /** * Updates the given constraint config. + * * @param config The constraint config to update * @return The updated constraint config * @throws NotFoundException If the constraint config does not exist @@ -109,6 +123,7 @@ public ConstraintConfig update(ConstraintConfig config) throws NotFoundException /** * Deletes the constraint config with the given id. + * * @param id The id of the constraint config to delete */ @Override @@ -116,23 +131,10 @@ public void delete(Long id) { constraintConfigEntityService.delete(id); } - /** - * Returns true if the site scope is enabled and the project scope is enabled for all projects or the project is in - * the list of enabled projects. - * @param project The project to check - * @param siteScope The site scope - * @param projectScope The project scope - * @return True if the site scope is enabled and the project scope is enabled for all projects or the project is in - * the list of enabled projects - */ - protected boolean isScopeEnabledForSiteAndProject(String project, ConstraintScope siteScope, ConstraintScope projectScope) { - return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled - projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list - } - /** * Validates that the given constraint config is valid. Throws an IllegalArgumentException if it is not. - * @param config + * + * @param config The constraint config to validate */ protected void validate(ConstraintConfig config) { if (config == null) { diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java index ff8102a..c2f74e8 100644 --- a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java @@ -7,6 +7,7 @@ import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; import org.nrg.xnat.compute.entities.ComputeEnvironmentHardwareOptionsEntity; import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.models.AccessScope; import org.nrg.xnat.compute.models.Hardware; import org.nrg.xnat.compute.models.HardwareConfig; import org.nrg.xnat.compute.models.HardwareScope; @@ -38,6 +39,7 @@ public DefaultHardwareConfigService(final HardwareConfigEntityService hardwareCo /** * Checks if a hardware config with the given id exists. + * * @param id The id of the hardware config to check for * @return True if a hardware config with the given id exists, false otherwise */ @@ -48,6 +50,7 @@ public boolean exists(Long id) { /** * Returns the hardware config with the given id. + * * @param id The id of the hardware config to retrieve * @return An optional containing the hardware config with the given id, or empty if no such hardware config exists */ @@ -59,6 +62,7 @@ public Optional retrieve(Long id) { /** * Returns all hardware configs + * * @return List of all hardware configs */ @Override @@ -72,6 +76,7 @@ public List retrieveAll() { /** * Creates a new hardware config. + * * @param hardwareConfig The hardware config to create * @return The newly created hardware config with an id */ @@ -96,6 +101,7 @@ public HardwareConfig create(HardwareConfig hardwareConfig) { /** * Updates an existing hardware config. + * * @param hardwareConfig The hardware config to update * @return The updated hardware config * @throws NotFoundException If no hardware config exists with the given id @@ -118,6 +124,7 @@ public HardwareConfig update(HardwareConfig hardwareConfig) throws NotFoundExcep /** * Deletes the hardware config with the given id. + * * @param id The id of the hardware config to delete * @throws NotFoundException If no hardware config exists with the given id */ @@ -139,14 +146,14 @@ public void delete(Long id) throws NotFoundException { } /** - * Checks if the hardware config with the given id is available for the given user and project. - * @param user The user to check for - * @param project The project to check for - * @param id The id of the hardware config to check for - * @return True if the hardware config with the given id is available for the given user and project, false otherwise. - */ + * Checks if the hardware config is available to the provided execution scope. + * + * @param id The id of the hardware config to check + * @param executionScope The execution scope to check the hardware config against + * @return True if the hardware config is available to the provided execution scope, false otherwise + **/ @Override - public boolean isAvailable(String user, String project, Long id) { + public boolean isAvailable(Long id, Map executionScope) { final Optional hardwareConfig = retrieve(id); if (!hardwareConfig.isPresent()) { @@ -154,21 +161,17 @@ public boolean isAvailable(String user, String project, Long id) { return false; } - final HardwareScope siteScope = hardwareConfig.get().getScopes().get(Scope.Site); - final HardwareScope projectScope = hardwareConfig.get().getScopes().get(Scope.Project); - final HardwareScope userScope = hardwareConfig.get().getScopes().get(Scope.User); - - return isScopeEnabledForSiteUserAndProject(user, project, siteScope, userScope, projectScope); - } + Map requiredScopes = hardwareConfig.get().getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - protected boolean isScopeEnabledForSiteUserAndProject(String user, String project, HardwareScope siteScope, HardwareScope userScope, HardwareScope projectScope) { - return siteScope != null && siteScope.isEnabled() && // Site scope must be enabled - userScope != null && (userScope.isEnabled() || userScope.getIds().contains(user)) && // User scope must be enabled for all users or the user must be in the list - projectScope != null && (projectScope.isEnabled() || projectScope.getIds().contains(project)); // Project scope must be enabled for all projects or the project must be in the list + return AccessScope.isEnabledFor(requiredScopes, executionScope); } /** * Validates the given hardware config. Throws an IllegalArgumentException if the hardware config is invalid. + * * @param hardwareConfig The hardware config to validate */ protected void validate(HardwareConfig hardwareConfig) { diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java index c65eb56..dc919c6 100644 --- a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java +++ b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java @@ -1,6 +1,7 @@ package org.nrg.xnat.compute.services.impl; import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.constants.Scope; import org.nrg.xnat.compute.models.*; import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; import org.nrg.xnat.compute.services.ConstraintConfigService; @@ -10,6 +11,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Service @@ -30,23 +32,22 @@ public DefaultJobTemplateService(final ComputeEnvironmentConfigService computeEn } /** - * Returns true if the specified compute environment config and hardware config are available for the specified user and - * project and the hardware config is allowed by the compute environment config. - * @param user the user - * @param project the project + * Returns true if the specified compute environment config and hardware config are available for the provided + * execution scope and the hardware config is allowed by the compute environment config. + * * @param computeEnvironmentConfigId the compute environment config id - * @param hardwareConfigId the hardware config id - * @return true if the specified compute environment config and hardware config are available for the specified user and - * project and the hardware config is allowed by the compute environment config + * @param hardwareConfigId the hardware config id + * @return true if the specified compute environment config and hardware config are available for the provided + * execution scope and the hardware config is allowed by the compute environment config */ @Override - public boolean isAvailable(String user, String project, Long computeEnvironmentConfigId, Long hardwareConfigId) { - if (user == null || project == null || computeEnvironmentConfigId == null || hardwareConfigId == null) { + public boolean isAvailable(Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope) { + if (computeEnvironmentConfigId == null || hardwareConfigId == null || executionScope == null) { throw new IllegalArgumentException("One or more parameters is null"); } - boolean isComputeEnvironmentConfigAvailable = computeEnvironmentConfigService.isAvailable(user, project, computeEnvironmentConfigId); - boolean isHardwareConfigAvailable = hardwareConfigService.isAvailable(user, project, hardwareConfigId); + boolean isComputeEnvironmentConfigAvailable = computeEnvironmentConfigService.isAvailable(computeEnvironmentConfigId, executionScope); + boolean isHardwareConfigAvailable = hardwareConfigService.isAvailable(hardwareConfigId, executionScope); if (!isComputeEnvironmentConfigAvailable || !isHardwareConfigAvailable) { return false; @@ -67,21 +68,22 @@ public boolean isAvailable(String user, String project, Long computeEnvironmentC } /** - * Returns a job template for the specified user, project, compute environment config, and hardware config. - * @param user the user - * @param project the project + * Returns a job template complete with compute environment, hardware, and constraints. + * * @param computeEnvironmentConfigId the compute environment config id - * @param hardwareConfigId the hardware config id - * @return a job template complete with compute environment and hardware + * @param hardwareConfigId the hardware config id + * @param executionScope the execution scope to verify the compute environment config and hardware are + * available + * @return a job template complete with compute environment, hardware, and constraints */ @Override - public JobTemplate resolve(String user, String project, Long computeEnvironmentConfigId, Long hardwareConfigId) { - if (user == null || project == null || computeEnvironmentConfigId == null || hardwareConfigId == null) { + public JobTemplate resolve(Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope) { + if (computeEnvironmentConfigId == null || hardwareConfigId == null || executionScope == null) { throw new IllegalArgumentException("One or more parameters is null"); } - if (!isAvailable(user, project, computeEnvironmentConfigId, hardwareConfigId)) { - throw new IllegalArgumentException("JobTemplate with user " + user + ", project " + project + ", computeEnvironmentConfigId " + computeEnvironmentConfigId + ", and hardwareConfigId " + hardwareConfigId + " is not available"); + if (!isAvailable(computeEnvironmentConfigId, hardwareConfigId, executionScope)) { + throw new IllegalArgumentException("JobTemplate resolution failed for computeEnvironmentConfigId " + computeEnvironmentConfigId + " and hardwareConfigId " + hardwareConfigId + " and executionScope " + executionScope); } ComputeEnvironmentConfig computeEnvironmentConfig = computeEnvironmentConfigService @@ -92,7 +94,7 @@ public JobTemplate resolve(String user, String project, Long computeEnvironmentC .retrieve(hardwareConfigId) .orElseThrow(() -> new IllegalArgumentException("HardwareConfig with id " + hardwareConfigId + " does not exist")); - List constraints = constraintConfigService.getAvailable(project).stream() + List constraints = constraintConfigService.getAvailable(executionScope).stream() .map(ConstraintConfig::getConstraint) .collect(Collectors.toList()); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java index 0e72dc4..e6bd63e 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java @@ -2,6 +2,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.nrg.framework.constants.Scope; import org.nrg.framework.services.NrgEventServiceI; import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xdat.security.services.UserManagementServiceI; @@ -166,7 +167,10 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) return; } - if (!jobTemplateService.isAvailable(user.getUsername(), projectId, computeEnvironmentConfigId, hardwareConfigId)) { + Map executionScope = new HashMap<>(); + executionScope.put(Scope.Project, projectId); + executionScope.put(Scope.User, user.getUsername()); + if (!jobTemplateService.isAvailable(computeEnvironmentConfigId, hardwareConfigId, executionScope)) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, "Failed to launch Jupyter notebook server. The job template either does not exist or is not available to the user and project.")); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java index d0ad733..ce32243 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java @@ -3,6 +3,7 @@ import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.nrg.framework.constants.Scope; import org.nrg.xdat.entities.AliasToken; import org.nrg.xdat.model.XnatSubjectassessordataI; import org.nrg.xdat.om.*; @@ -133,7 +134,9 @@ public Map getSubjectPaths(final UserI user, final List final String path = ((XnatExperimentdata) subjectAssessor).getCurrentSessionFolder(true); // Experiments - subjectPaths.put("/data/projects/" + xnatProjectdata.getId() + "/experiments/" + assessorLabel, path); + if (Files.exists(Paths.get(path))) { + subjectPaths.put("/data/projects/" + xnatProjectdata.getId() + "/experiments/" + assessorLabel, path); + } } catch (BaseXnatExperimentdata.UnknownPrimaryProjectException | InvalidArchiveStructure e) { // Container service ignores this error. log.error("", e); @@ -164,7 +167,10 @@ public Map getExperimentPaths(final UserI user, final List executionScope = new HashMap<>(); + executionScope.put(Scope.Project, projectId); + executionScope.put(Scope.User, user.getUsername()); + JobTemplate jobTemplate = jobTemplateService.resolve(computeEnvironmentConfigId, hardwareConfigId, executionScope); // specific xsi type -> general xsi type if (instanceOf(xsiType, XnatExperimentdata.SCHEMA_ELEMENT_NAME)) { diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-environment-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-environment-configs.js index 01f0157..869fe56 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-environment-configs.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-environment-configs.js @@ -120,12 +120,22 @@ XNAT.compute.computeEnvironmentConfigs = getObject(XNAT.compute.computeEnvironme throw new Error(`Error deleting compute environment config ${id}`); } } - - XNAT.compute.computeEnvironmentConfigs.available = async (type, user, project) => { - console.debug(`Fetching available compute environment configs for type ${type} and user ${user} and project ${project}`); - const url = type ? - XNAT.url.csrfUrl(`/xapi/compute-environment-configs/available?type=${type}&user=${user}&project=${project}`) : - XNAT.url.csrfUrl(`/xapi/compute-environment-configs/available?user=${user}&project=${project}`); + + XNAT.compute.computeEnvironmentConfigs.available = async (type, executionScope) => { + console.debug(`Fetching available compute environment configs for type ${type} and execution scope ${executionScope}`) + let url = type ? + XNAT.url.csrfUrl(`/xapi/compute-environment-configs/available?type=${type}`): + XNAT.url.csrfUrl(`/xapi/compute-environment-configs/available?`); + + // add each scope to the url as a query parameter + for (const scope in executionScope) { + if (executionScope.hasOwnProperty(scope)) { + const value = executionScope[scope]; + if (value) { + url += `&${scope}=${value}`; + } + } + } const response = await fetch(url, { method: 'GET', @@ -648,7 +658,6 @@ XNAT.compute.computeEnvironmentConfigs = getObject(XNAT.compute.computeEnvironme let validateImageEl = XNAT.validate(imageEl).reset().chain(); validateImageEl.is('notEmpty').failure('Image is required'); validators.push(validateImageEl); - // TODO: Validate image format let envVarsPresent = document.querySelectorAll('input.key').length > 0; if (envVarsPresent) { @@ -702,6 +711,7 @@ XNAT.compute.computeEnvironmentConfigs = getObject(XNAT.compute.computeEnvironme Site: { scope: 'Site', enabled: siteEnabledEl.checked, + ids: [], }, Project: { scope: 'Project', diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/hardware-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/hardware-configs.js index 7ecdef0..6bbf31f 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/hardware-configs.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/hardware-configs.js @@ -734,7 +734,6 @@ XNAT.compute.hardwareConfigs = getObject(XNAT.compute.hardwareConfigs || {}); } // Sort the hardware configs by name - // TODO Sort by name? Sort by something else? hardwareConfigs = hardwareConfigs.sort((a, b) => { if (a.name < b.name) { return -1; diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js index 00c3dee..f0264a6 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js @@ -176,7 +176,13 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s console.debug(`jupyterhub-servers.js: XNAT.plugin.jupyterhub.servers.startServer`); console.debug(`Launching jupyter server. User: ${username}, Server Name: ${servername}, XSI Type: ${xsiType}, ID: ${itemId}, Label: ${itemLabel}, Project ID: ${projectId}, eventTrackingId: ${eventTrackingId}`); - XNAT.compute.computeEnvironmentConfigs.available("JUPYTERHUB", username, projectId).then(computeEnvironmentConfigs => { + const executionScope = { + 'site': 'XNAT', + 'user': username, + 'prj': projectId, + } + + XNAT.compute.computeEnvironmentConfigs.available("JUPYTERHUB", executionScope).then(computeEnvironmentConfigs => { const cancelButton = { label: 'Cancel', isDefault: false, diff --git a/src/test/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApiTest.java b/src/test/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApiTest.java index bc4a678..879a36d 100644 --- a/src/test/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApiTest.java +++ b/src/test/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApiTest.java @@ -6,6 +6,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.nrg.framework.constants.Scope; import org.nrg.xnat.compute.config.ComputeEnvironmentConfigsApiConfig; import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; @@ -25,6 +26,8 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.util.NestedServletException; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import static org.mockito.Mockito.*; @@ -216,7 +219,8 @@ public void testGetAvailableComputeEnvironmentConfigs() throws Exception { final MockHttpServletRequestBuilder request = MockMvcRequestBuilders .get("/compute-environment-configs/available") .param("user", mockUser.getLogin()) - .param("project", "projectId") + .param("prj", "projectId") + .param("datatype", "xnat:mrSessionData") .param("type", "JUPYTERHUB") .with(authentication(mockAuthentication)) .with(csrf()) @@ -225,8 +229,11 @@ public void testGetAvailableComputeEnvironmentConfigs() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); // Verify that the service was called - verify(mockComputeEnvironmentConfigService, times(1)).getAvailable(eq(mockUser.getLogin()), eq("projectId"), eq(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB)); + Map expectedScopeMap = new HashMap<>(); + expectedScopeMap.put(Scope.User, mockUser.getLogin()); + expectedScopeMap.put(Scope.Project, "projectId"); + expectedScopeMap.put(Scope.DataType, "xnat:mrSessionData"); + verify(mockComputeEnvironmentConfigService, times(1)).getAvailable(eq(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB), eq(expectedScopeMap)); } - } \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigServiceTest.java index 556f28a..90e271a 100644 --- a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigServiceTest.java @@ -40,6 +40,7 @@ public class DefaultComputeEnvironmentConfigServiceTest { private HardwareConfig hardwareConfig1; private HardwareConfig hardwareConfig2; + private HardwareConfig hardwareConfig3; @Before public void before() { @@ -153,7 +154,12 @@ public void testGetAvailable_WrongUser() { commitTransaction(); // Test - List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable("User2", "Project1", null); + Map executionScope = new HashMap<>(); + executionScope.put(User, "User2"); + executionScope.put(Project, "Project1"); + executionScope.put(Experiment, "XNAT_E00001"); + executionScope.put(DataType, "xnat:petSessionData"); + List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable(null, executionScope); // Verify assertThat(computeEnvironmentConfigs.size(), is(1)); @@ -172,7 +178,12 @@ public void testGetAvailable_WrongProject() { commitTransaction(); // Test - List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable("User1", "Project2"); + Map executionScope = new HashMap<>(); + executionScope.put(User, "User1"); + executionScope.put(Project, "Project2"); + executionScope.put(Experiment, "XNAT_E00001"); + executionScope.put(DataType, "xnat:petSessionData"); + List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable(executionScope); // Verify assertThat(computeEnvironmentConfigs.size(), is(1)); @@ -182,6 +193,16 @@ public void testGetAvailable_WrongProject() { @Test @DirtiesContext public void testGetAvailable() { + // Create hardware configs + HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); + HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); + HardwareConfigEntity hardwareConfigEntity3 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig3)); + commitTransaction(); + + hardwareConfig1.setId(hardwareConfigEntity1.getId()); + hardwareConfig2.setId(hardwareConfigEntity2.getId()); + hardwareConfig3.setId(hardwareConfigEntity3.getId()); + // Create ComputeEnvironmentConfigs ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); @@ -191,11 +212,20 @@ public void testGetAvailable() { commitTransaction(); // Test - List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable("User1", "Project1"); + Map executionScope = new HashMap<>(); + executionScope.put(User, "User1"); + executionScope.put(Project, "Project1"); + executionScope.put(Experiment, "XNAT_E00001"); + executionScope.put(DataType, "xnat:ctSessionData"); + List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable(executionScope); // Verify assertThat(computeEnvironmentConfigs.size(), is(2)); - assertThat(computeEnvironmentConfigs, hasItems(created1, created2)); + assertThat(computeEnvironmentConfigs.get(0).getHardwareOptions().getHardwareConfigs().size(), is(2)); + assertThat(computeEnvironmentConfigs.get(0).getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2)); + assertThat(computeEnvironmentConfigs.get(0).getHardwareOptions().getHardwareConfigs(), not(hasItems(hardwareConfig3))); + assertThat(computeEnvironmentConfigs.get(1).getHardwareOptions().getHardwareConfigs().size(), is(1)); + assertThat(computeEnvironmentConfigs.get(1).getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig2)); } @Test @@ -210,7 +240,12 @@ public void testGetAvailable_SpecificType() { commitTransaction(); // Test - List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable("User1", "Project1", CONTAINER_SERVICE); + Map executionScope = new HashMap<>(); + executionScope.put(User, "User1"); + executionScope.put(Project, "Project1"); + executionScope.put(Experiment, "XNAT_E00001"); + executionScope.put(DataType, "xnat:petSessionData"); + List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable(CONTAINER_SERVICE, executionScope); // Verify assertThat(computeEnvironmentConfigs.size(), is(1)); @@ -229,19 +264,24 @@ public void testIsAvailable() { commitTransaction(); // Test - boolean result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project1", created1.getId()); + Map executionScope = new HashMap<>(); + executionScope.put(User, "User1"); + executionScope.put(Project, "Project1"); + executionScope.put(Experiment, "XNAT_E00001"); + executionScope.put(DataType, "xnat:petSessionData"); + boolean result = defaultComputeEnvironmentConfigService.isAvailable(created1.getId(), executionScope); // Verify assertTrue(result); // Test - result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project1", created2.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable(created2.getId(), executionScope); // Verify assertTrue(result); // Test - result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project1", created3.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable(created3.getId(), executionScope); // Verify assertFalse(result); @@ -259,19 +299,24 @@ public void testNotAvailable_WrongUser() { commitTransaction(); // Test - boolean result = defaultComputeEnvironmentConfigService.isAvailable("User2", "Project1", created1.getId()); + Map executionScope = new HashMap<>(); + executionScope.put(User, "User2"); + executionScope.put(Project, "Project1"); + executionScope.put(Experiment, "XNAT_E00001"); + executionScope.put(DataType, "xnat:petSessionData"); + boolean result = defaultComputeEnvironmentConfigService.isAvailable(created1.getId(), executionScope); // Verify assertTrue(result); // Test - result = defaultComputeEnvironmentConfigService.isAvailable("User2", "Project1", created2.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable(created2.getId(), executionScope); // Verify assertFalse(result); // Test - result = defaultComputeEnvironmentConfigService.isAvailable("User2", "Project1", created3.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable(created3.getId(), executionScope); // Verify assertFalse(result); @@ -289,19 +334,59 @@ public void testNotAvailable_WrongProject() { commitTransaction(); // Test - boolean result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project2", created1.getId()); + Map executionScope = new HashMap<>(); + executionScope.put(User, "User1"); + executionScope.put(Project, "Project2"); + executionScope.put(Experiment, "XNAT_E00001"); + executionScope.put(DataType, "xnat:petSessionData"); + boolean result = defaultComputeEnvironmentConfigService.isAvailable(created1.getId(), executionScope); // Verify assertTrue(result); // Test - result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project2", created2.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable(created2.getId(), executionScope); // Verify assertFalse(result); // Test - result = defaultComputeEnvironmentConfigService.isAvailable("User1", "Project2", created3.getId()); + result = defaultComputeEnvironmentConfigService.isAvailable(created3.getId(), executionScope); + + // Verify + assertFalse(result); + } + + @Test + @DirtiesContext + public void testNotAvailable_WrongDataType() { + // Create ComputeEnvironmentConfigs + ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); + commitTransaction(); + ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); + commitTransaction(); + ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); + commitTransaction(); + + // Test + Map executionScope = new HashMap<>(); + executionScope.put(User, "User1"); + executionScope.put(Project, "Project1"); + executionScope.put(Experiment, "XNAT_E00001"); + executionScope.put(DataType, "xnat:projectData"); // Wrong data type + boolean result = defaultComputeEnvironmentConfigService.isAvailable(created1.getId(), executionScope); + + // Verify + assertTrue(result); + + // Test + result = defaultComputeEnvironmentConfigService.isAvailable(created2.getId(), executionScope); + + // Verify + assertFalse(result); + + // Test + result = defaultComputeEnvironmentConfigService.isAvailable(created3.getId(), executionScope); // Verify assertFalse(result); @@ -313,18 +398,20 @@ public void testCreate_AllowAllHardware() { // First create hardware configs HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); + HardwareConfigEntity hardwareConfigEntity3 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig3)); commitTransaction(); hardwareConfig1.setId(hardwareConfigEntity1.getId()); hardwareConfig2.setId(hardwareConfigEntity2.getId()); + hardwareConfig3.setId(hardwareConfigEntity3.getId()); // Next create compute environment config ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); commitTransaction(); // Verify that all hardware configs are associated with the compute environment config - assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(2)); - assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2)); + assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(3)); + assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2, hardwareConfig3)); } @Test @@ -333,18 +420,20 @@ public void testCreate_SelectHardware() { // First create hardware configs HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); + HardwareConfigEntity hardwareConfigEntity3 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig3)); commitTransaction(); hardwareConfig1.setId(hardwareConfigEntity1.getId()); hardwareConfig2.setId(hardwareConfigEntity2.getId()); + hardwareConfig3.setId(hardwareConfigEntity3.getId()); // Next create compute environment config ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); commitTransaction(); // Verify that only the selected hardware config is associated with the compute environment config - assertThat(created2.getHardwareOptions().getHardwareConfigs().size(), is(1)); - assertThat(created2.getHardwareOptions().getHardwareConfigs(), hasItem(hardwareConfig2)); + assertThat(created2.getHardwareOptions().getHardwareConfigs().size(), is(2)); + assertThat(created2.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig2, hardwareConfig3)); } @Test @@ -579,6 +668,50 @@ public void createDummyConfigs() { .scopes(hardwareScopes2) .build(); + // Setup second hardware config + Hardware hardware3 = Hardware.builder() + .name("Large") + .cpuReservation(8.0) + .cpuLimit(8.0) + .memoryReservation("16G") + .memoryLimit("16G") + .build(); + + // No constraints, environment variables or generic resources + hardware3.setConstraints(Collections.emptyList()); + hardware3.setEnvironmentVariables(Collections.emptyList()); + hardware3.setGenericResources(Collections.emptyList()); + + // Setup hardware scopes + HardwareScope hardwareSiteScope3 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope3 = HardwareScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("ProjectABCDE"))) + .build(); + + HardwareScope userHardwareScope3 = HardwareScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map hardwareScopes3 = new HashMap<>(); + hardwareScopes3.put(Site, hardwareSiteScope3); + hardwareScopes3.put(Project, hardwareProjectScope3); + hardwareScopes3.put(User, userHardwareScope3); + + // Setup second hardware config entity + hardwareConfig3 = HardwareConfig.builder() + .hardware(hardware3) + .scopes(hardwareScopes3) + .build(); + // Setup first compute environment ComputeEnvironment computeEnvironment1 = ComputeEnvironment.builder() .name("Jupyter Datascience Notebook") @@ -612,7 +745,7 @@ public void createDummyConfigs() { ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions1 = ComputeEnvironmentHardwareOptions.builder() .allowAllHardware(true) - .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig1, hardwareConfig2))) + .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig1, hardwareConfig2, hardwareConfig3))) .build(); computeEnvironmentConfig1 = ComputeEnvironmentConfig.builder() @@ -639,23 +772,30 @@ public void createDummyConfigs() { ComputeEnvironmentScope computeEnvironmentProjectScope2 = ComputeEnvironmentScope.builder() .scope(Project) .enabled(false) - .ids(new HashSet<>(Collections.singletonList("Project1"))) + .ids(new HashSet<>(new ArrayList<>(Arrays.asList("Project1", "Project10", "Project100")))) .build(); ComputeEnvironmentScope computeEnvironmentUserScope2 = ComputeEnvironmentScope.builder() .scope(User) .enabled(false) - .ids(new HashSet<>(Collections.singletonList("User1"))) + .ids(new HashSet<>(new ArrayList<>(Arrays.asList("User1", "User10", "User100")))) + .build(); + + ComputeEnvironmentScope computeEnvironmentDatatypeScope2 = ComputeEnvironmentScope.builder() + .scope(DataType) + .enabled(false) + .ids(new HashSet<>(Arrays.asList("xnat:mrSessionData", "xnat:petSessionData", "xnat:ctSessionData"))) .build(); Map computeEnvironmentScopes2 = new HashMap<>(); computeEnvironmentScopes2.put(Site, computeEnvironmentSiteScope2); computeEnvironmentScopes2.put(Project, computeEnvironmentProjectScope2); computeEnvironmentScopes2.put(User, computeEnvironmentUserScope2); + computeEnvironmentScopes2.put(DataType, computeEnvironmentDatatypeScope2); ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions2 = ComputeEnvironmentHardwareOptions.builder() .allowAllHardware(false) - .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig2))) + .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig2, hardwareConfig3))) .build(); computeEnvironmentConfig2 = ComputeEnvironmentConfig.builder() diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigServiceTest.java index e1b0381..5af7261 100644 --- a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigServiceTest.java @@ -135,21 +135,25 @@ public void testGetAvailable() { commitTransaction(); // Test - List retrieved = constraintConfigService.getAvailable("ProjectA"); + Map executionScopes = new HashMap<>(); + executionScopes.put(Scope.Project, "ProjectA"); + List retrieved = constraintConfigService.getAvailable(executionScopes); // Verify assertEquals(2, retrieved.size()); assertThat(retrieved, hasItems(created1, created3)); // Test - retrieved = constraintConfigService.getAvailable("ProjectB"); + executionScopes.put(Scope.Project, "ProjectB"); + retrieved = constraintConfigService.getAvailable(executionScopes); // Verify assertEquals(2, retrieved.size()); assertThat(retrieved, hasItems(created1, created3)); // Test - retrieved = constraintConfigService.getAvailable("ProjectC"); + executionScopes.put(Scope.Project, "ProjectC"); + retrieved = constraintConfigService.getAvailable(executionScopes); // Verify assertEquals(1, retrieved.size()); diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java index d709089..e876dbd 100644 --- a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java @@ -183,8 +183,11 @@ public void testIsAvailable() { commitTransaction(); // Test - boolean isAvailable1 = defaultHardwareConfigService.isAvailable("User1", "Project1", created1.getId()); - boolean isAvailable2 = defaultHardwareConfigService.isAvailable("User1", "Project1", created2.getId()); + Map executionScope = new HashMap<>(); + executionScope.put(User, "User1"); + executionScope.put(Project, "Project1"); + boolean isAvailable1 = defaultHardwareConfigService.isAvailable(created1.getId(), executionScope); + boolean isAvailable2 = defaultHardwareConfigService.isAvailable(created2.getId(), executionScope); // Verify assertTrue(isAvailable1); @@ -200,8 +203,11 @@ public void testIsAvailable_WrongUser() { commitTransaction(); // Test - boolean isAvailable1 = defaultHardwareConfigService.isAvailable("User2", "Project1", created1.getId()); - boolean isAvailable2 = defaultHardwareConfigService.isAvailable("User2", "Project1", created2.getId()); + Map executionScope = new HashMap<>(); + executionScope.put(User, "User2"); + executionScope.put(Project, "Project1"); + boolean isAvailable1 = defaultHardwareConfigService.isAvailable(created1.getId(), executionScope); + boolean isAvailable2 = defaultHardwareConfigService.isAvailable(created2.getId(), executionScope); // Verify assertTrue(isAvailable1); @@ -217,8 +223,11 @@ public void testIsAvailable_WrongProject() { commitTransaction(); // Test - boolean isAvailable1 = defaultHardwareConfigService.isAvailable("User1", "Project2", created1.getId()); - boolean isAvailable2 = defaultHardwareConfigService.isAvailable("User1", "Project2", created2.getId()); + Map executionScope = new HashMap<>(); + executionScope.put(User, "User1"); + executionScope.put(Project, "Project2"); + boolean isAvailable1 = defaultHardwareConfigService.isAvailable(created1.getId(), executionScope); + boolean isAvailable2 = defaultHardwareConfigService.isAvailable(created2.getId(), executionScope); // Verify assertTrue(isAvailable1); diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java index 293d7b5..8a0b2a8 100644 --- a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java +++ b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java @@ -5,6 +5,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.nrg.framework.constants.Scope; import org.nrg.xnat.compute.models.*; import org.nrg.xnat.compute.config.DefaultJobTemplateServiceTestConfig; import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; @@ -14,10 +15,7 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Optional; +import java.util.*; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; @@ -48,11 +46,18 @@ public void after() { @Test public void testIsAvailable_ComputeEnvironmentNotAvailable() { // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(false); - when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(false); + when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(true); + + Map executionScope = new HashMap<>(); + executionScope.put(Scope.Site, "site"); + executionScope.put(Scope.Project, "project"); + executionScope.put(Scope.User, "user"); + executionScope.put(Scope.DataType, "xnat:petSessionData"); + executionScope.put(Scope.Experiment, "XNAT_E00001"); // Run - boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); + boolean isAvailable = jobTemplateService.isAvailable(1L, 1L, executionScope); // Verify assertFalse(isAvailable); @@ -61,11 +66,15 @@ public void testIsAvailable_ComputeEnvironmentNotAvailable() { @Test public void testIsAvailable_HardwareNotAvailable() { // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(true); - when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(false); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(true); + when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(false); + + Map executionScope = new HashMap<>(); + executionScope.put(Scope.Project, "project"); + executionScope.put(Scope.User, "user"); // Run - boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); + boolean isAvailable = jobTemplateService.isAvailable(1L, 1L, executionScope); // Verify assertFalse(isAvailable); @@ -74,11 +83,14 @@ public void testIsAvailable_HardwareNotAvailable() { @Test public void testIsAvailable_ComputeEnvironmentAndHardwareNotAvailable() { // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(false); - when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(false); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(false); + when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(false); // Run - boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); + Map executionScope = new HashMap<>(); + executionScope.put(Scope.Project, "project"); + executionScope.put(Scope.User, "user"); + boolean isAvailable = jobTemplateService.isAvailable(1L, 1L, executionScope); // Verify assertFalse(isAvailable); @@ -87,8 +99,8 @@ public void testIsAvailable_ComputeEnvironmentAndHardwareNotAvailable() { @Test public void testIsAvailable_ComputeEnvironmentConfigAllHardwareIsAvailable() { // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(true); - when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(true); + when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(true); ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().build(); ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); @@ -96,18 +108,22 @@ public void testIsAvailable_ComputeEnvironmentConfigAllHardwareIsAvailable() { computeEnvironmentConfig.setHardwareOptions(hardwareOptions); when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); + Map executionScope = new HashMap<>(); + executionScope.put(Scope.Project, "project"); + executionScope.put(Scope.User, "user"); + // Run - boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); + boolean isAvailable = jobTemplateService.isAvailable(1L, 1L, executionScope); // Verify assertTrue(isAvailable); } @Test - public void testIsAvailable_ComputeEnvironmentConfigEnvironmentificHardwareAllowed() { + public void testIsAvailable_ComputeEnvironmentConfigSpecificHardwareAllowed() { // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(true); - when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(true); + when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(true); ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().id(1L).build(); ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); @@ -117,18 +133,24 @@ public void testIsAvailable_ComputeEnvironmentConfigEnvironmentificHardwareAllow hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); + Map executionScope = new HashMap<>(); + executionScope.put(Scope.Project, "project"); + executionScope.put(Scope.User, "user"); + executionScope.put(Scope.DataType, "xnat:petSessionData"); + executionScope.put(Scope.Experiment, "XNAT_E00001"); + // Run - boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 1L); + boolean isAvailable = jobTemplateService.isAvailable(1L, 1L, executionScope); // Verify assertTrue(isAvailable); } @Test - public void testIsAvailable_ComputeEnvironmentConfigEnvironmentificHardwareNotAllowed() { + public void testIsAvailable_ComputeEnvironmentConfigSpecificHardwareNotAllowed() { // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(true); - when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(true); + when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(true); ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().id(1L).build(); ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); @@ -138,8 +160,14 @@ public void testIsAvailable_ComputeEnvironmentConfigEnvironmentificHardwareNotAl hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); + Map executionScope = new HashMap<>(); + executionScope.put(Scope.Project, "project"); + executionScope.put(Scope.User, "user"); + executionScope.put(Scope.DataType, "xnat:petSessionData"); + executionScope.put(Scope.Experiment, "XNAT_E00002"); + // Run - boolean isAvailable = jobTemplateService.isAvailable("user", "project", 1L, 2L); + boolean isAvailable = jobTemplateService.isAvailable(1L, 2L, executionScope); // Verify assertFalse(isAvailable); @@ -148,8 +176,8 @@ public void testIsAvailable_ComputeEnvironmentConfigEnvironmentificHardwareNotAl @Test public void testResolve() { // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any(), any())).thenReturn(true); - when(mockHardwareConfigService.isAvailable(any(), any(), any())).thenReturn(true); + when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(true); + when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(true); ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().id(1L).build(); ComputeEnvironment computeEnvironment = ComputeEnvironment.builder() @@ -184,8 +212,12 @@ public void testResolve() { when(mockHardwareConfigService.retrieve(any())).thenReturn(Optional.of(hardwareConfig)); when(mockConstraintConfigService.getAvailable(any())).thenReturn(Collections.singletonList(constraintConfig)); + Map executionScope = new HashMap<>(); + executionScope.put(Scope.Project, "project"); + executionScope.put(Scope.User, "user"); + // Run - JobTemplate jobTemplate = jobTemplateService.resolve("user", "project", 1L, 1L); + JobTemplate jobTemplate = jobTemplateService.resolve(1L, 1L, executionScope); // Verify assertNotNull(jobTemplate); @@ -194,5 +226,4 @@ public void testResolve() { assertEquals(Collections.singletonList(constraint), jobTemplate.getConstraints()); } - } \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java index 63e936f..a94d060 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java @@ -374,7 +374,7 @@ public void testStartServer_jobTemplateUnavailable() throws Exception { when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); // Job template unavailable - when(mockJobTemplateService.isAvailable(any(), any(), any(), any())).thenReturn(false); + when(mockJobTemplateService.isAvailable(any(), any(), any())).thenReturn(false); // Test jupyterHubService.startServer(user, startProjectRequest); @@ -399,7 +399,7 @@ public void testStartServer_Timeout() throws UserNotFoundException, ResourceAlre when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); // Job template is available - when(mockJobTemplateService.isAvailable(any(), any(), any(), any())).thenReturn(true); + when(mockJobTemplateService.isAvailable(any(), any(), any())).thenReturn(true); // To successfully start a server there should be no running servers at first. // A subsequent call should contain a server, but let's presume it is never ready @@ -434,7 +434,7 @@ public void testStartServer_Success() throws Exception { when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); // Job template is available - when(mockJobTemplateService.isAvailable(any(), any(), any(), any())).thenReturn(true); + when(mockJobTemplateService.isAvailable(any(), any(), any())).thenReturn(true); // To successfully start a server there should be no running servers at first. // A subsequent call should eventually return a server in the ready state @@ -466,7 +466,7 @@ public void testStartServer_CreateUser_Success() throws Exception { when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); // Job template is available - when(mockJobTemplateService.isAvailable(any(), any(), any(), any())).thenReturn(true); + when(mockJobTemplateService.isAvailable(any(), any(), any())).thenReturn(true); // To successfully start a server there should be no running servers at first. // A subsequent call should eventually return a server in the ready state From 4657e268cb2208dec451b4796baa23003d88636c Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Tue, 27 Jun 2023 12:26:44 -0500 Subject: [PATCH 26/49] Bump to 1.0.0-RC2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b1da4ca..af42f44 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "1.0.0-RC2-SNAPSHOT" +version "1.0.0-RC2" description "JupyterHub Plugin for XNAT" repositories { From 25ee71ad1618de581b3ccdf8770984d0ad5db9d2 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Fri, 30 Jun 2023 15:55:58 -0500 Subject: [PATCH 27/49] JHP-65: Improves error messages involving the XNAT service account token and JupyterHub service account user password --- .../initialize/JupyterHubUserInitializer.java | 42 +++++--- .../impl/DefaultJupyterHubService.java | 52 ++++++++-- .../utils/JupyterHubServiceAccountHelper.java | 7 ++ ...DefaultJupyterHubServiceAccountHelper.java | 56 +++++++++++ .../xnat/plugin/jupyterhub/jupyterhub-hub.js | 4 +- ...yterHubServiceAccountHelperTestConfig.java | 23 +++++ .../DefaultJupyterHubServiceConfig.java | 7 +- .../plugins/jupyterhub/config/MockConfig.java | 6 ++ .../impl/DefaultJupyterHubServiceTest.java | 26 +++++ ...ultJupyterHubServiceAccountHelperTest.java | 99 +++++++++++++++++++ 10 files changed, 294 insertions(+), 28 deletions(-) create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/JupyterHubServiceAccountHelper.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultJupyterHubServiceAccountHelper.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceAccountHelperTestConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultJupyterHubServiceAccountHelperTest.java diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java index c0afb4a..eff1c4c 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java @@ -50,20 +50,20 @@ public String getTaskName() { */ @Override protected void callImpl() throws InitializingTaskException { - log.debug("Initializing JupyterHub user."); + log.info("Creating `jupyterhub` user account."); if (!xftManagerHelper.isInitialized()) { - log.debug("XFT not initialized, deferring execution."); + log.info("XFT not initialized, deferring execution."); throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); } if (!appInfo.isInitialized()) { - log.debug("XNAT not initialized, deferring execution."); + log.info("XNAT not initialized, deferring execution."); throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); } if (userManagementService.exists("jupyterhub")) { - log.debug("JupyterHub user already exists."); + log.info("`jupyterhub` user account already exists, skipping creation."); return; } @@ -76,6 +76,14 @@ protected void callImpl() throws InitializingTaskException { jupyterhubUser.setEnabled(false); jupyterhubUser.setVerified(true); + String errorMessage = "Unable to create the `jupyterhub` user account. " + + "This account is used by JupyterHub to communicate with XNAT. " + + "You will need to create this account manually in the UI. " + + "The default username is 'jupyterhub', the password is 'jupyterhub' (but you should change this), " + + "and the account must have the JupyterHub service account role. " + + "This accounts credentials must be added to the JupyterHub configuration/environment variables as well. " + + "Please see the documentation for more information."; + try { userManagementService.save(jupyterhubUser, Users.getAdminUser(), false, new EventDetails(EventUtils.CATEGORY.DATA, @@ -84,25 +92,31 @@ false, new EventDetails(EventUtils.CATEGORY.DATA, "Requested by the JupyterHub Plugin", "Created new user " + jupyterhubUser.getUsername())); } catch (Exception e) { - throw new InitializingTaskException(InitializingTaskException.Level.Error, - "Error occurred creating user " + jupyterhubUser.getLogin(), - e); + log.error(errorMessage, e); + throw new InitializingTaskException(InitializingTaskException.Level.Error, errorMessage, e); } try { boolean added = roleHolder.addRole(Users.getAdminUser(), jupyterhubUser, "JupyterHub"); if (!added) { - throw new InitializingTaskException(InitializingTaskException.Level.Error, - "Error occurred adding the JupyterHub role to the jupyterhub user account."); + log.error(errorMessage); + throw new InitializingTaskException(InitializingTaskException.Level.Error, errorMessage); } } catch (Exception e) { - // The user can be deleted, but it is disabled. - throw new InitializingTaskException(InitializingTaskException.Level.Error, - "Error occurred adding the JupyterHub role to the jupyterhub user account.", - e); + log.error(errorMessage, e); + throw new InitializingTaskException(InitializingTaskException.Level.Error, errorMessage, e); } - log.info("Created jupyterhub user."); + log.info("Successfully created `jupyterhub` user account."); } + + @Override + public boolean isMaxedOut() { + // The execution count is incremented regardless of whether XNAT has been initialized or not, padding the count + // to account for this. 12 * 15 seconds = 3 minutes seems reasonable. + int maxExecutions = 12; + return super.executions() >= maxExecutions; + } + } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java index e6bd63e..c48bce3 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java @@ -22,6 +22,7 @@ import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; import org.nrg.xnatx.plugins.jupyterhub.services.JupyterHubService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; +import org.nrg.xnatx.plugins.jupyterhub.utils.JupyterHubServiceAccountHelper; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -44,6 +45,7 @@ public class DefaultJupyterHubService implements JupyterHubService { private final JupyterHubPreferences jupyterHubPreferences; private final UserManagementServiceI userManagementService; private final JobTemplateService jobTemplateService; + private final JupyterHubServiceAccountHelper jupyterHubServiceAccountHelper; @Autowired public DefaultJupyterHubService(final JupyterHubClient jupyterHubClient, @@ -52,7 +54,8 @@ public DefaultJupyterHubService(final JupyterHubClient jupyterHubClient, final UserOptionsService userOptionsService, final JupyterHubPreferences jupyterHubPreferences, final UserManagementServiceI userManagementService, - final JobTemplateService jobTemplateService) { + final JobTemplateService jobTemplateService, + final JupyterHubServiceAccountHelper jupyterHubServiceAccountHelper) { this.jupyterHubClient = jupyterHubClient; this.eventService = eventService; this.permissionsHelper = permissionsHelper; @@ -60,6 +63,7 @@ public DefaultJupyterHubService(final JupyterHubClient jupyterHubClient, this.jupyterHubPreferences = jupyterHubPreferences; this.userManagementService = userManagementService; this.jobTemplateService = jobTemplateService; + this.jupyterHubServiceAccountHelper = jupyterHubServiceAccountHelper; } /** @@ -163,7 +167,7 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) if (!permissionsHelper.canRead(user, projectId, itemId, xsiType)) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Failed to launch Jupyter notebook server. Permission denied.")); + "Failed to launch Jupyter notebook server. Permission denied to read " + xsiType + " " + itemId + " in project " + projectId)); return; } @@ -173,18 +177,36 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) if (!jobTemplateService.isAvailable(computeEnvironmentConfigId, hardwareConfigId, executionScope)) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Failed to launch Jupyter notebook server. The job template either does not exist or is not available to the user and project.")); + "Failed to launch Jupyter notebook server. The compute environment or hardware configuration is not available to the user.")); return; } CompletableFuture.runAsync(() -> { - // Check if JupyterHub is online + // getVersion() does not require authentication, check if JupyterHub is online try { jupyterHubClient.getVersion(); } catch (Exception e) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Unable to connect to JupyterHub")); + "Failed to connect to JupyterHub. Please ensure the following:\n" + + "(1) JupyterHub is running \n" + + "(2) Verify the correct API URL is set in the plugin settings." + )); + return; + } + + // getInfo() does require authentication, check if XNAT can connect and authenticate with JupyterHub + try { + jupyterHubClient.getInfo(); + } catch (Exception e) { + eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, + JupyterServerEventI.Operation.Start, + "Failed to connect to JupyterHub. Please check the following: \n" + + "(1) Ensure that JupyterHub is running. \n" + + "(2) Verify the correct API URL is set in the plugin settings. \n" + + "(3) Confirm XNATs token for authenticating with JupyterHub is correctly set in " + + "both the plugin settings and JupyterHub configuration." + )); return; } @@ -203,7 +225,8 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, "Failed to launch Jupyter notebook server. " + - "There is already one running.")); + "There is already one running. " + + "Please stop the running server before starting a new one.")); return; } @@ -259,7 +282,9 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Failed to launch Jupyter notebook server. Timeout reached.")); + "Failed to launch Jupyter notebook server. " + + "Timeout exceeded while waiting for JupyterHub to spawn server. " + + "Check the XNAT and JupyterHub system logs for error messages.")); } catch (UserNotFoundException e) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, @@ -267,14 +292,21 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) } catch (ResourceAlreadyExistsException e) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Failed to launch Jupyter notebook server. Resource already exists.")); + "Failed to launch Jupyter notebook server. A server with the same name is already running.")); } catch (InterruptedException e) { - String msg = "Failed to launch Jupyter notebook server. Thread interrupted.."; + String msg = "Failed to launch Jupyter notebook server. Thread interrupted. Check the XNAT and JupyterHub system logs for error messages."; eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, msg)); log.error(msg, e); } catch (Exception e) { - String msg = "Failed to launch Jupyter notebook server. See system logs for detailed error."; + String msg = "Failed to launch Jupyter notebook server. "; + + if (!jupyterHubServiceAccountHelper.isJupyterHubServiceAccountEnabled()) { + msg += "Make sure the JupyterHub service account user is enabled and provide the credentials to JupyterHub (refer to the documentation for more details). "; + } + + msg += "Check the XNAT and JupyterHub system logs for error messages."; + eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, msg)); log.error(msg, e); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/JupyterHubServiceAccountHelper.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/JupyterHubServiceAccountHelper.java new file mode 100644 index 0000000..a6c81c8 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/JupyterHubServiceAccountHelper.java @@ -0,0 +1,7 @@ +package org.nrg.xnatx.plugins.jupyterhub.utils; + +public interface JupyterHubServiceAccountHelper { + + boolean isJupyterHubServiceAccountEnabled(); + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultJupyterHubServiceAccountHelper.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultJupyterHubServiceAccountHelper.java new file mode 100644 index 0000000..d6e9b73 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultJupyterHubServiceAccountHelper.java @@ -0,0 +1,56 @@ +package org.nrg.xnatx.plugins.jupyterhub.utils.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xft.security.UserI; +import org.nrg.xnatx.plugins.jupyterhub.utils.JupyterHubServiceAccountHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collection; + +@Service +@Slf4j +public class DefaultJupyterHubServiceAccountHelper implements JupyterHubServiceAccountHelper { + + private final UserManagementServiceI userManagementService; + private final RoleServiceI roleService; + + @Autowired + public DefaultJupyterHubServiceAccountHelper(final UserManagementServiceI userManagementService, + final RoleServiceI roleService) { + this.userManagementService = userManagementService; + this.roleService = roleService; + } + + /** + * Checks if the JupyterHub service account is enabled. The JupyterHub service account is considered enabled if + * any of the users with the JupyterHub role are enabled. Without this account, JupyterHub will not be able to + * communicate with XNAT. An administrator is responsible for supplying the credentials for the service account to + * JupyterHub as environment variables. + * + * @return true if a JupyterHub service account is enabled, false otherwise + */ + @Override + public boolean isJupyterHubServiceAccountEnabled() { + Collection jupyterHubServiceAccountRoleHolders = roleService.getUsers("JupyterHub"); + + if (jupyterHubServiceAccountRoleHolders == null || jupyterHubServiceAccountRoleHolders.isEmpty()) { + return false; + } + + // If any of the users with the JupyterHub role are enabled, then the JupyterHub service account is + // considered enabled. The credentials for the JupyterHub service account still need to be added to the + // JupyterHub configuration environment variables by an administrator. + return jupyterHubServiceAccountRoleHolders.stream().map(username -> { + try { + UserI user = userManagementService.getUser(username); + return user.isEnabled(); + } catch (Exception e) { + return false; + } + }).reduce(false, (a, b) -> a || b); + } + +} diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js index 0336471..e7c351c 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-hub.js @@ -139,7 +139,7 @@ XNAT.plugin.jupyterhub.hub = getObject(XNAT.plugin.jupyterhub.hub || {}); let status = 'version' in hubInfo; hubTable.tr() .td([spawn('div.left', [hubPreferences['jupyterHubApiUrl']])]) - .td([spawn(status ? 'div.center.success' : 'div.center.warning', [status ? 'OK' : 'Down'])]) + .td([spawn(status ? 'div.center.success' : 'div.center.warning', [status ? 'Connected' : 'Connection Error'])]) .td([spawn('div.center', [hubInfo['version']])]) .td([spawn('div.center', [editButton()])]); }).catch(e => { @@ -147,7 +147,7 @@ XNAT.plugin.jupyterhub.hub = getObject(XNAT.plugin.jupyterhub.hub || {}); hubTable.tr() .td([spawn('div.left', ['Unable to connect to JupyterHub'])]) - .td([spawn('div.center.warning', ['Down'])]) + .td([spawn('div.center.warning', ['Connection Error'])]) .td([spawn('div.center', [])]) .td([spawn('div.center', [editButton()])]); }) diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceAccountHelperTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceAccountHelperTestConfig.java new file mode 100644 index 0000000..bf3deaf --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceAccountHelperTestConfig.java @@ -0,0 +1,23 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnatx.plugins.jupyterhub.utils.impl.DefaultJupyterHubServiceAccountHelper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({MockConfig.class}) +public class DefaultJupyterHubServiceAccountHelperTestConfig { + + @Bean + public DefaultJupyterHubServiceAccountHelper defaultJupyterHubServiceAccountHelper(final UserManagementServiceI mockUserManagementService, + final RoleServiceI mockRoleService) { + return new DefaultJupyterHubServiceAccountHelper( + mockUserManagementService, + mockRoleService + ); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java index 54518f3..0a4e9eb 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java @@ -7,6 +7,7 @@ import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultJupyterHubService; +import org.nrg.xnatx.plugins.jupyterhub.utils.JupyterHubServiceAccountHelper; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -23,14 +24,16 @@ public DefaultJupyterHubService defaultJupyterHubService(final JupyterHubClient final UserOptionsService mockUserOptionsService, final JupyterHubPreferences mockJupyterHubPreferences, final UserManagementServiceI mockUserManagementService, - final JobTemplateService mockJobTemplateService) { + final JobTemplateService mockJobTemplateService, + final JupyterHubServiceAccountHelper mockJupyterHubServiceAccountHelper) { return new DefaultJupyterHubService(mockJupyterHubClient, mockNrgEventService, mockPermissionsHelper, mockUserOptionsService, mockJupyterHubPreferences, mockUserManagementService, - mockJobTemplateService); + mockJobTemplateService, + mockJupyterHubServiceAccountHelper); } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java index 6c73b87..7344f77 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java @@ -17,6 +17,7 @@ import org.nrg.xnatx.plugins.jupyterhub.client.JupyterHubClient; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; import org.nrg.xnatx.plugins.jupyterhub.services.*; +import org.nrg.xnatx.plugins.jupyterhub.utils.JupyterHubServiceAccountHelper; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; import org.springframework.beans.factory.annotation.Qualifier; @@ -158,4 +159,9 @@ public HardwareConfigService mockHardwareConfigService() { public JobTemplateService mockJobTemplateService() { return Mockito.mock(JobTemplateService.class); } + + @Bean + public JupyterHubServiceAccountHelper mockJupyterHubServiceAccountHelper() { + return Mockito.mock(JupyterHubServiceAccountHelper.class); + } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java index a94d060..ac3162e 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java @@ -343,6 +343,32 @@ public void testStartServer_HubOffline() throws Exception { verify(mockJupyterHubClient, never()).startServer(any(), any(), any()); } + @Test(timeout = 2000) + public void testStartServer_HubOnlineButXNATCantConnect() throws Exception { + // Grant permissions + when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); + + // Can't getInfo from JupyterHub, but connection to JupyterHub succeeded + when(mockJupyterHubClient.getVersion()).thenReturn(null); + when(mockJupyterHubClient.getInfo()).thenThrow(RuntimeException.class); + + // Test + jupyterHubService.startServer(user, startProjectRequest); + Thread.sleep(1000); // Async call, need to wait. Is there a better way to test this? + + // Verify failure to start event occurred + verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); + JupyterServerEventI capturedEvent = jupyterServerEventCaptor.getValue(); + assertEquals(JupyterServerEventI.Status.Failed, capturedEvent.getStatus()); + assertEquals(JupyterServerEventI.Operation.Start, capturedEvent.getOperation()); + + // Verify user options are not saved + verify(mockUserOptionsEntityService, never()).createOrUpdate(any()); + + // Verify no attempts to start a server + verify(mockJupyterHubClient, never()).startServer(any(), any(), any()); + } + @Test(timeout = 2000) public void testStartServer_serverAlreadyRunning() throws Exception { // Grant permissions diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultJupyterHubServiceAccountHelperTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultJupyterHubServiceAccountHelperTest.java new file mode 100644 index 0000000..2830ebc --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultJupyterHubServiceAccountHelperTest.java @@ -0,0 +1,99 @@ +package org.nrg.xnatx.plugins.jupyterhub.utils.impl; + +import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xft.security.UserI; +import org.nrg.xnatx.plugins.jupyterhub.config.DefaultJupyterHubServiceAccountHelperTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Arrays; +import java.util.Collections; + +import static org.mockito.Mockito.when; + + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = DefaultJupyterHubServiceAccountHelperTestConfig.class) +public class DefaultJupyterHubServiceAccountHelperTest extends TestCase { + + @Autowired private UserManagementServiceI mockUserManagementService; + @Autowired private RoleServiceI mockRoleService; + + @Autowired private DefaultJupyterHubServiceAccountHelper defaultJupyterHubServiceAccountHelper; + + @Test + public void testServiceAccountDisabled_noUsersWithRole() { + when(mockRoleService.getUsers("JupyterHub")).thenReturn(Collections.emptyList()); + assertFalse(defaultJupyterHubServiceAccountHelper.isJupyterHubServiceAccountEnabled()); + } + + @Test + public void testServiceAccountDisabled_userWithRoleButNotEnabled() throws Exception { + // Setup mock user. User has role but is not enabled. + UserI mockUser = Mockito.mock(UserI.class); + when(mockUser.getLogin()).thenReturn("jupyterhub"); + when(mockUser.isEnabled()).thenReturn(false); + when(mockUserManagementService.getUser("jupyterhub")).thenReturn(mockUser); + when(mockRoleService.getUsers("JupyterHub")).thenReturn(Collections.singletonList("jupyterhub")); + + // Test + assertFalse(defaultJupyterHubServiceAccountHelper.isJupyterHubServiceAccountEnabled()); + } + + @Test + public void testServiceAccountEnabled_userWithRoleAndEnabled() throws Exception { + // Setup mock user. User has role and is enabled. + UserI mockUser = Mockito.mock(UserI.class); + when(mockUser.getLogin()).thenReturn("jupyterhub"); + when(mockUser.isEnabled()).thenReturn(true); + when(mockUserManagementService.getUser("jupyterhub")).thenReturn(mockUser); + when(mockRoleService.getUsers("JupyterHub")).thenReturn(Collections.singletonList("jupyterhub")); + + // Test + assertTrue(defaultJupyterHubServiceAccountHelper.isJupyterHubServiceAccountEnabled()); + } + + @Test + public void testServiceAccountEnabled_multipleUsersWithRole() throws Exception { + // Setup mock users. One user has role and is enabled. Another user has role but is not enabled. + UserI mockUser = Mockito.mock(UserI.class); + when(mockUser.getLogin()).thenReturn("jupyterhub"); + when(mockUser.isEnabled()).thenReturn(false); + when(mockUserManagementService.getUser("jupyterhub")).thenReturn(mockUser); + + UserI mockUser2 = Mockito.mock(UserI.class); + when(mockUser2.getLogin()).thenReturn("jupyterhub-service-account"); + when(mockUser2.isEnabled()).thenReturn(true); + when(mockUserManagementService.getUser("jupyterhub-service-account")).thenReturn(mockUser2); + + when(mockRoleService.getUsers("JupyterHub")).thenReturn(Arrays.asList("jupyterhub", "jupyterhub-service-account")); + + // Test + assertTrue(defaultJupyterHubServiceAccountHelper.isJupyterHubServiceAccountEnabled()); + } + + @Test + public void testServiceAccountEnabled_multipleUsersWithRoleButNoEnabledUsers() throws Exception { + // Setup mock users. One user has role but is not enabled. Another user has role but is not enabled. + UserI mockUser = Mockito.mock(UserI.class); + when(mockUser.getLogin()).thenReturn("jupyterhub"); + when(mockUser.isEnabled()).thenReturn(false); + when(mockUserManagementService.getUser("jupyterhub")).thenReturn(mockUser); + + UserI mockUser2 = Mockito.mock(UserI.class); + when(mockUser2.getLogin()).thenReturn("jupyterhub-service-account"); + when(mockUser2.isEnabled()).thenReturn(false); + when(mockUserManagementService.getUser("jupyterhub-service-account")).thenReturn(mockUser2); + + when(mockRoleService.getUsers("JupyterHub")).thenReturn(Arrays.asList("jupyterhub", "jupyterhub-service-account")); + + // Test + assertFalse(defaultJupyterHubServiceAccountHelper.isJupyterHubServiceAccountEnabled()); + } +} \ No newline at end of file From 039d7440047b198f6b429976e992454e794d8d72 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Fri, 30 Jun 2023 15:57:06 -0500 Subject: [PATCH 28/49] Bump version to 1.0.0-RC3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index af42f44..4ce9dfc 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "1.0.0-RC2" +version "1.0.0-RC3" description "JupyterHub Plugin for XNAT" repositories { From 8657bda090c4ef1eb27b257e6204cf83f592f9f7 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 3 Jul 2023 12:41:36 -0500 Subject: [PATCH 29/49] Merge main --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4ce9dfc..0ffca57 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "1.0.0-RC3" +version "1.0.0" description "JupyterHub Plugin for XNAT" repositories { From 08d077d2fe313d30b005be162692359af6f1b7d3 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 3 Jul 2023 12:42:30 -0500 Subject: [PATCH 30/49] Update version to 1.1.0-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0ffca57..b2aae2a 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "1.0.0" +version "1.1.0-SNAPSHOT" description "JupyterHub Plugin for XNAT" repositories { From 5aa263f173d162af83e4289741b8cc814cca4e7a Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Wed, 16 Aug 2023 17:59:31 -0500 Subject: [PATCH 31/49] JHP-66 and XNAT-7867: Migrating new compute services from plugin to core --- build.gradle | 4 +- .../xnat/compute/config/ComputeConfig.java | 12 - .../ComputeEnvironmentConfigEntity.java | 144 --- .../entities/ComputeEnvironmentEntity.java | 146 --- ...mputeEnvironmentHardwareOptionsEntity.java | 114 -- .../ComputeEnvironmentScopeEntity.java | 113 -- .../entities/ConstraintConfigEntity.java | 101 -- .../compute/entities/ConstraintEntity.java | 88 -- .../entities/ConstraintScopeEntity.java | 115 -- .../entities/EnvironmentVariableEntity.java | 53 - .../entities/GenericResourceEntity.java | 53 - .../entities/HardwareConfigEntity.java | 111 -- .../entities/HardwareConstraintEntity.java | 79 -- .../xnat/compute/entities/HardwareEntity.java | 184 --- .../compute/entities/HardwareScopeEntity.java | 116 -- .../xnat/compute/entities/MountEntity.java | 75 -- .../nrg/xnat/compute/models/AccessScope.java | 49 - .../compute/models/ComputeEnvironment.java | 23 - .../models/ComputeEnvironmentConfig.java | 30 - .../ComputeEnvironmentHardwareOptions.java | 19 - .../models/ComputeEnvironmentScope.java | 21 - .../nrg/xnat/compute/models/Constraint.java | 54 - .../xnat/compute/models/ConstraintConfig.java | 22 - .../xnat/compute/models/ConstraintScope.java | 21 - .../compute/models/EnvironmentVariable.java | 17 - .../xnat/compute/models/GenericResource.java | 17 - .../org/nrg/xnat/compute/models/Hardware.java | 26 - .../xnat/compute/models/HardwareConfig.java | 22 - .../xnat/compute/models/HardwareScope.java | 21 - .../nrg/xnat/compute/models/JobTemplate.java | 20 - .../org/nrg/xnat/compute/models/Mount.java | 19 - .../ComputeEnvironmentConfigDao.java | 112 -- .../repositories/ConstraintConfigDao.java | 68 -- .../repositories/HardwareConfigDao.java | 85 -- .../rest/ComputeEnvironmentConfigsApi.java | 143 --- .../compute/rest/ConstraintConfigsApi.java | 115 -- .../xnat/compute/rest/HardwareConfigsApi.java | 112 -- ...ComputeEnvironmentConfigEntityService.java | 15 - .../ComputeEnvironmentConfigService.java | 24 - .../ConstraintConfigEntityService.java | 8 - .../services/ConstraintConfigService.java | 20 - .../services/HardwareConfigEntityService.java | 8 - .../services/HardwareConfigService.java | 21 - .../compute/services/JobTemplateService.java | 13 - ...efaultComputeEnvironmentConfigService.java | 389 ------ .../impl/DefaultConstraintConfigService.java | 195 --- .../impl/DefaultHardwareConfigService.java | 230 ---- .../impl/DefaultJobTemplateService.java | 109 -- ...ComputeEnvironmentConfigEntityService.java | 77 -- ...ibernateConstraintConfigEntityService.java | 14 - .../HibernateHardwareConfigEntityService.java | 14 - .../plugins/jupyterhub/JupyterHubPlugin.java | 4 +- .../compute/compute-environment-configs.js | 1022 ---------------- .../xnat/plugin/compute/constraint-configs.js | 610 --------- .../xnat/plugin/compute/hardware-configs.js | 1085 ----------------- .../screens/topBar/Jupyter/Default.vm | 4 +- .../spawner/jupyterhub/site-settings.yaml | 8 +- .../ComputeEnvironmentConfigsApiConfig.java | 47 - .../ConstraintConfigsApiTestConfig.java | 46 - ...uteEnvironmentConfigServiceTestConfig.java | 23 - ...aultConstraintConfigServiceTestConfig.java | 20 - ...efaultHardwareConfigServiceTestConfig.java | 23 - .../DefaultJobTemplateServiceTestConfig.java | 26 - .../config/HardwareConfigsApiConfig.java | 47 - ...ironmentConfigEntityServiceTestConfig.java | 79 -- .../xnat/compute/config/HibernateConfig.java | 79 -- ...nstraintConfigEntityServiceTestConfig.java | 61 - .../config/HibernateEntityServicesConfig.java | 60 - ...HardwareConfigEntityServiceTestConfig.java | 61 - .../nrg/xnat/compute/config/MockConfig.java | 106 -- .../compute/config/ObjectMapperConfig.java | 29 - .../compute/config/RestApiTestConfig.java | 86 -- .../xnat/compute/models/ConstraintTest.java | 35 - .../ComputeEnvironmentConfigsApiTest.java | 239 ---- .../rest/ConstraintConfigsApiTest.java | 206 ---- .../compute/rest/HardwareConfigsApiTest.java | 201 --- ...ltComputeEnvironmentConfigServiceTest.java | 875 ------------- .../DefaultConstraintConfigServiceTest.java | 278 ----- .../DefaultHardwareConfigServiceTest.java | 480 -------- .../impl/DefaultJobTemplateServiceTest.java | 229 ---- ...uteEnvironmentConfigEntityServiceTest.java | 505 -------- ...nateConstraintConfigEntityServiceTest.java | 90 -- ...ernateHardwareConfigEntityServiceTest.java | 23 - .../nrg/xnat/compute/utils/TestingUtils.java | 15 - 84 files changed, 9 insertions(+), 10354 deletions(-) delete mode 100644 src/main/java/org/nrg/xnat/compute/config/ComputeConfig.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentConfigEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentHardwareOptionsEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentScopeEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/ConstraintConfigEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/ConstraintEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/ConstraintScopeEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/EnvironmentVariableEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/GenericResourceEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/HardwareConfigEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/HardwareConstraintEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/HardwareEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/HardwareScopeEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/entities/MountEntity.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/AccessScope.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/ComputeEnvironment.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentConfig.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentHardwareOptions.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentScope.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/Constraint.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/ConstraintConfig.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/ConstraintScope.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/EnvironmentVariable.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/GenericResource.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/Hardware.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/HardwareConfig.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/HardwareScope.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/JobTemplate.java delete mode 100644 src/main/java/org/nrg/xnat/compute/models/Mount.java delete mode 100644 src/main/java/org/nrg/xnat/compute/repositories/ComputeEnvironmentConfigDao.java delete mode 100644 src/main/java/org/nrg/xnat/compute/repositories/ConstraintConfigDao.java delete mode 100644 src/main/java/org/nrg/xnat/compute/repositories/HardwareConfigDao.java delete mode 100644 src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java delete mode 100644 src/main/java/org/nrg/xnat/compute/rest/ConstraintConfigsApi.java delete mode 100644 src/main/java/org/nrg/xnat/compute/rest/HardwareConfigsApi.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigEntityService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/ConstraintConfigEntityService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/ConstraintConfigService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/HardwareConfigEntityService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/HardwareConfigService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityService.java delete mode 100644 src/main/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityService.java delete mode 100644 src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-environment-configs.js delete mode 100644 src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/constraint-configs.js delete mode 100644 src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/hardware-configs.js delete mode 100644 src/test/java/org/nrg/xnat/compute/config/ComputeEnvironmentConfigsApiConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/ConstraintConfigsApiTestConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/DefaultComputeEnvironmentConfigServiceTestConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/DefaultConstraintConfigServiceTestConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/DefaultHardwareConfigServiceTestConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/DefaultJobTemplateServiceTestConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/HardwareConfigsApiConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/HibernateComputeEnvironmentConfigEntityServiceTestConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/HibernateConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/HibernateConstraintConfigEntityServiceTestConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/HibernateEntityServicesConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/HibernateHardwareConfigEntityServiceTestConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/MockConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/ObjectMapperConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/config/RestApiTestConfig.java delete mode 100644 src/test/java/org/nrg/xnat/compute/models/ConstraintTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApiTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/rest/ConstraintConfigsApiTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/rest/HardwareConfigsApiTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigServiceTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigServiceTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityServiceTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityServiceTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityServiceTest.java delete mode 100644 src/test/java/org/nrg/xnat/compute/utils/TestingUtils.java diff --git a/build.gradle b/build.gradle index b2aae2a..5bbc4d1 100644 --- a/build.gradle +++ b/build.gradle @@ -14,9 +14,9 @@ version "1.1.0-SNAPSHOT" description "JupyterHub Plugin for XNAT" repositories { + mavenLocal() maven { url "https://nrgxnat.jfrog.io/nrgxnat/libs-release" } maven { url "https://nrgxnat.jfrog.io/nrgxnat/libs-snapshot" } - mavenLocal() mavenCentral() maven { url "https://www.dcm4che.org/maven2" } } @@ -28,7 +28,7 @@ configurations { } dependencies { - xnatProvided platform("org.nrg:parent:1.8.8.1") + xnatProvided platform("org.nrg:parent:1.8.10-SNAPSHOT") xnatProvided "org.nrg:framework" xnatProvided "org.nrg.xnat:xnat-data-models" xnatProvided "org.nrg.xnat:web" diff --git a/src/main/java/org/nrg/xnat/compute/config/ComputeConfig.java b/src/main/java/org/nrg/xnat/compute/config/ComputeConfig.java deleted file mode 100644 index 4cf6877..0000000 --- a/src/main/java/org/nrg/xnat/compute/config/ComputeConfig.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; - -@Configuration -@ComponentScan({"org.nrg.xnat.compute.services.impl", - "org.nrg.xnat.compute.services", - "org.nrg.xnat.compute.rest", - "org.nrg.xnat.compute.repositories"}) -public class ComputeConfig { -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentConfigEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentConfigEntity.java deleted file mode 100644 index e8990f8..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentConfigEntity.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.framework.constants.Scope; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; - -import javax.persistence.*; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -@Entity -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode(callSuper = true) -public class ComputeEnvironmentConfigEntity extends AbstractHibernateEntity { - - private Set configTypes; - - private ComputeEnvironmentEntity computeEnvironment; - private Map scopes; - private ComputeEnvironmentHardwareOptionsEntity hardwareOptions; - - @ElementCollection - public Set getConfigTypes() { - if (configTypes == null) { - configTypes = new HashSet<>(); - } - - return configTypes; - } - - public void setConfigTypes(Set configTypes) { - this.configTypes = configTypes; - } - - @OneToOne(mappedBy = "computeEnvironmentConfig", cascade = CascadeType.ALL, orphanRemoval = true) - public ComputeEnvironmentEntity getComputeEnvironment() { - return computeEnvironment; - } - - public void setComputeEnvironment(ComputeEnvironmentEntity computeEnvironment) { - computeEnvironment.setComputeEnvironmentConfig(this); - this.computeEnvironment = computeEnvironment; - } - - @OneToMany(mappedBy = "computeEnvironmentConfig", cascade = CascadeType.ALL, orphanRemoval = true) - public Map getScopes() { - return scopes; - } - - public void setScopes(Map scopes) { - this.scopes = scopes; - } - - @OneToOne(mappedBy = "computeEnvironmentConfig", cascade = CascadeType.ALL, orphanRemoval = true) - public ComputeEnvironmentHardwareOptionsEntity getHardwareOptions() { - return hardwareOptions; - } - - public void setHardwareOptions(ComputeEnvironmentHardwareOptionsEntity hardwareOptions) { - this.hardwareOptions = hardwareOptions; - } - - /** - * Creates a new entity from the pojo. - * @param pojo The pojo to create the entity from - * @return The newly created entity - */ - public static ComputeEnvironmentConfigEntity fromPojo(final ComputeEnvironmentConfig pojo) { - final ComputeEnvironmentConfigEntity entity = new ComputeEnvironmentConfigEntity(); - entity.update(pojo); - return entity; - } - - /** - * Creates a new pojo from the entity. - * @return The pojo created from the entity - */ - public ComputeEnvironmentConfig toPojo() { - return ComputeEnvironmentConfig.builder() - .id(getId()) - .configTypes(getConfigTypes() - .stream() - .map(ComputeEnvironmentConfig.ConfigType::valueOf) - .collect(Collectors.toSet())) - .computeEnvironment(getComputeEnvironment().toPojo()) - .scopes(getScopes() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toPojo()))) - .hardwareOptions(getHardwareOptions().toPojo()) - .build(); - } - - /** - * Updates the entity with the values from the pojo. Does not update the hardwareConfigs, since that is a - * many-to-many relationship and needs to be handled separately. - * @param pojo The pojo to update the entity with - */ - public void update(final ComputeEnvironmentConfig pojo) { - setConfigTypes(pojo.getConfigTypes() - .stream() - .map(Enum::name) - .collect(Collectors.toSet())); - - if (getComputeEnvironment() == null) { - // This is a new entity, so we need to create the computeEnvironment entity - setComputeEnvironment(ComputeEnvironmentEntity.fromPojo(pojo.getComputeEnvironment())); - } else { - // This is an existing entity, so we need to update the computeEnvironment entity - getComputeEnvironment().update(pojo.getComputeEnvironment()); - } - - if (getScopes() == null) { - // This is a new entity, so we need to create the scopes entity - setScopes(pojo.getScopes() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> ComputeEnvironmentScopeEntity.fromPojo(e.getValue())))); - } else { - // This is an existing entity, so we need to update the scopes entities - getScopes().forEach((key, value) -> value.update(pojo.getScopes().get(key))); - } - - // Set the computeEnvironmentConfig on the scopes - getScopes().values().forEach(s -> s.setComputeEnvironmentConfig(this)); - - if (getHardwareOptions() == null) { - // This is a new entity, so we need to create the hardwareOptions entity - ComputeEnvironmentHardwareOptionsEntity computeEnvironmentHardwareOptionsEntity = ComputeEnvironmentHardwareOptionsEntity.fromPojo(pojo.getHardwareOptions()); - setHardwareOptions(computeEnvironmentHardwareOptionsEntity); - computeEnvironmentHardwareOptionsEntity.setComputeEnvironmentConfig(this); - } else { - // This is an existing entity, so we need to update the hardwareOptions entity - getHardwareOptions().update(pojo.getHardwareOptions()); - } - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentEntity.java deleted file mode 100644 index 5c0e110..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentEntity.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.xnat.compute.models.ComputeEnvironment; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -@Entity -@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"name"})}) -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode(callSuper = true) -public class ComputeEnvironmentEntity extends AbstractHibernateEntity { - - private String name; - private String image; - private String command; - - private List environmentVariables; - private List mounts; - - @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeEnvironmentConfigEntity computeEnvironmentConfig; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getImage() { - return image; - } - - public void setImage(String image) { - this.image = image; - } - - public String getCommand() { - return command; - } - - public void setCommand(String command) { - this.command = command; - } - - @ElementCollection - public List getEnvironmentVariables() { - if (environmentVariables == null) { - environmentVariables = new ArrayList<>(); - } - - return environmentVariables; - } - - public void setEnvironmentVariables(List environmentVariables) { - this.environmentVariables = environmentVariables; - } - - @ElementCollection - public List getMounts() { - if (mounts == null) { - mounts = new ArrayList<>(); - } - - return mounts; - } - - public void setMounts(List mounts) { - this.mounts = mounts; - } - - @OneToOne - public ComputeEnvironmentConfigEntity getComputeEnvironmentConfig() { - return computeEnvironmentConfig; - } - - public void setComputeEnvironmentConfig(ComputeEnvironmentConfigEntity computeEnvironmentConfig) { - this.computeEnvironmentConfig = computeEnvironmentConfig; - } - - /** - * Creates a new entity from the given pojo. - * @param pojo The pojo to convert. - * @return The entity created from the pojo. - */ - public static ComputeEnvironmentEntity fromPojo(ComputeEnvironment pojo) { - final ComputeEnvironmentEntity entity = new ComputeEnvironmentEntity(); - entity.update(pojo); - return entity; - } - - /** - * Converts this entity to a pojo. - * @return The pojo created from this entity. - */ - public ComputeEnvironment toPojo() { - return ComputeEnvironment.builder() - .name(getName()) - .image(getImage()) - .command(getCommand()) - .environmentVariables(getEnvironmentVariables().stream().map(EnvironmentVariableEntity::toPojo).collect(Collectors.toList())) - .mounts(getMounts().stream().map(MountEntity::toPojo).collect(Collectors.toList())) - .build(); - } - - /** - * Updates this entity from the given pojo. - * @param pojo The pojo to update from. - */ - public void update(ComputeEnvironment pojo) { - setName(pojo.getName()); - setImage(pojo.getImage()); - setCommand(pojo.getCommand()); - - // Clear the existing environment variables - getEnvironmentVariables().clear(); - - // Add the new environment variables - if (pojo.getEnvironmentVariables() != null) { - getEnvironmentVariables().addAll(pojo.getEnvironmentVariables() - .stream() - .map(EnvironmentVariableEntity::fromPojo) - .collect(Collectors.toList())); - } - - // Clear the existing mounts - getMounts().clear(); - - // Add the new mounts - if (pojo.getMounts() != null) { - getMounts().addAll(pojo.getMounts() - .stream() - .map(MountEntity::fromPojo) - .collect(Collectors.toList())); - } - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentHardwareOptionsEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentHardwareOptionsEntity.java deleted file mode 100644 index c3c5ee9..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentHardwareOptionsEntity.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.xnat.compute.models.ComputeEnvironmentHardwareOptions; - -import javax.persistence.*; -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Collectors; - -@Entity -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode -public class ComputeEnvironmentHardwareOptionsEntity { - - private long id; - - @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeEnvironmentConfigEntity computeEnvironmentConfig; - - private boolean allowAllHardware; - private Set hardwareConfigs; - - @Id - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - @OneToOne - @MapsId - public ComputeEnvironmentConfigEntity getComputeEnvironmentConfig() { - return computeEnvironmentConfig; - } - - public void setComputeEnvironmentConfig(ComputeEnvironmentConfigEntity computeEnvironmentConfig) { - this.computeEnvironmentConfig = computeEnvironmentConfig; - } - - public boolean isAllowAllHardware() { - return allowAllHardware; - } - - public void setAllowAllHardware(boolean allowAllHardware) { - this.allowAllHardware = allowAllHardware; - } - - @ManyToMany - @JoinTable(name = "compute_environment_hardware_options_hardware_config", - joinColumns = @JoinColumn(name = "compute_environment_hardware_options_id"), - inverseJoinColumns = @JoinColumn(name = "hardware_config_id")) - public Set getHardwareConfigs() { - return hardwareConfigs; - } - - public void setHardwareConfigs(Set hardwareConfigs) { - this.hardwareConfigs = hardwareConfigs; - } - - public void addHardwareConfig(HardwareConfigEntity hardwareConfig) { - if (hardwareConfigs == null) { - hardwareConfigs = new HashSet<>(); - } - - hardwareConfigs.add(hardwareConfig); - } - - public void removeHardwareConfig(HardwareConfigEntity hardwareConfig) { - if (hardwareConfigs != null) { - hardwareConfigs.remove(hardwareConfig); - } - } - - /** - * Creates a new entity from the given pojo. - * @param pojo The pojo to create the entity from. - * @return The newly created entity. - */ - public static ComputeEnvironmentHardwareOptionsEntity fromPojo(final ComputeEnvironmentHardwareOptions pojo) { - final ComputeEnvironmentHardwareOptionsEntity entity = new ComputeEnvironmentHardwareOptionsEntity(); - entity.update(pojo); - return entity; - } - - /** - * Creates a new pojo from the given entity. - * @return The newly created pojo. - */ - public ComputeEnvironmentHardwareOptions toPojo() { - return ComputeEnvironmentHardwareOptions.builder() - .allowAllHardware(allowAllHardware) - .hardwareConfigs(hardwareConfigs.stream().map(HardwareConfigEntity::toPojo).collect(Collectors.toSet())) - .build(); - } - - /** - * Updates the entity with the values from the given pojo. - * @param pojo The pojo to update the entity with. - */ - public void update(final ComputeEnvironmentHardwareOptions pojo) { - setAllowAllHardware(pojo.isAllowAllHardware()); - - if (hardwareConfigs == null) { - hardwareConfigs = new HashSet<>(); - } - - // Updating the hardware configs is handled separately by the add/remove hardware config methods - } -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentScopeEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentScopeEntity.java deleted file mode 100644 index 4a7ef9c..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/ComputeEnvironmentScopeEntity.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.models.ComputeEnvironmentScope; - -import javax.persistence.*; -import java.util.HashSet; -import java.util.Set; - -@Entity -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode -public class ComputeEnvironmentScopeEntity { - - private long id; - private String scope; - private boolean enabled; - private Set ids; - - @ToString.Exclude @EqualsAndHashCode.Exclude private ComputeEnvironmentConfigEntity computeEnvironmentConfig; - - @Id - @GeneratedValue - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getScope() { - return scope; - } - - public void setScope(String scope) { - this.scope = scope; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @ElementCollection - public Set getIds() { - if (ids == null) { - ids = new HashSet<>(); - } - - return ids; - } - - public void setIds(Set ids) { - this.ids = ids; - } - - @ManyToOne - public ComputeEnvironmentConfigEntity getComputeEnvironmentConfig() { - return computeEnvironmentConfig; - } - - public void setComputeEnvironmentConfig(ComputeEnvironmentConfigEntity computeEnvironmentConfig) { - this.computeEnvironmentConfig = computeEnvironmentConfig; - } - - /** - * Updates this entity with the values from the given pojo. - * @param pojo The pojo to update from - */ - public void update(ComputeEnvironmentScope pojo) { - setScope(pojo.getScope().name()); - setEnabled(pojo.isEnabled()); - - getIds().clear(); - - // add new ids - if (pojo.getIds() != null && !pojo.getIds().isEmpty()) { - getIds().addAll(pojo.getIds()); - } - } - - /** - * Converts this entity to a pojo. - * @return The pojo - */ - public ComputeEnvironmentScope toPojo() { - return ComputeEnvironmentScope.builder() - .scope(Scope.valueOf(getScope())) - .enabled(isEnabled()) - .ids(getIds()) - .build(); - } - - /** - * Creates a new entity from the given pojo. - * @param pojo The pojo to create from - * @return The new entity - */ - public static ComputeEnvironmentScopeEntity fromPojo(ComputeEnvironmentScope pojo) { - final ComputeEnvironmentScopeEntity entity = new ComputeEnvironmentScopeEntity(); - entity.update(pojo); - return entity; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/ConstraintConfigEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ConstraintConfigEntity.java deleted file mode 100644 index 09436da..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/ConstraintConfigEntity.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.framework.constants.Scope; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.xnat.compute.models.ConstraintConfig; - -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; -import java.util.Map; -import java.util.stream.Collectors; - -@Entity -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode(callSuper = true) -public class ConstraintConfigEntity extends AbstractHibernateEntity { - - private ConstraintEntity constraint; - private Map scopes; - - @OneToOne(mappedBy = "constraintConfig", cascade = CascadeType.ALL, orphanRemoval = true) - public ConstraintEntity getConstraint() { - return constraint; - } - - public void setConstraint(ConstraintEntity constraint) { - constraint.setConstraintConfig(this); - this.constraint = constraint; - } - - @OneToMany(mappedBy = "constraintConfig", cascade = CascadeType.ALL, orphanRemoval = true) - public Map getScopes() { - return scopes; - } - - public void setScopes(Map scopes) { - this.scopes = scopes; - } - - /** - * This method is used to update the entity from the pojo. - * @param pojo The pojo to update from. - */ - public void update(final ConstraintConfig pojo) { - if (getConstraint() == null) { - // New entity - setConstraint(ConstraintEntity.fromPojo(pojo.getConstraint())); - } else { - // Existing entity - getConstraint().update(pojo.getConstraint()); - } - - getConstraint().setConstraintConfig(this); - - if (getScopes() == null) { - // This is a new entity, so we need to create the scope entities - setScopes(pojo.getScopes() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> ConstraintScopeEntity.fromPojo(e.getValue())))); - } else { - // This is an existing entity, so we need to update the scope entities - getScopes().forEach((key, value) -> value.update(pojo.getScopes().get(key))); - } - - // Set the constraint config on the scope entities - getScopes().forEach((key, value) -> value.setConstraintConfig(this)); - } - - /** - * This method is used to convert the entity to a pojo. - * @return The pojo. - */ - public ConstraintConfig toPojo() { - return ConstraintConfig.builder() - .id(getId()) - .constraint(getConstraint().toPojo()) - .scopes(getScopes() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toPojo()))) - .build(); - } - - /** - * This method is used to create a new entity from the pojo. - * @param pojo The pojo to create from. - * @return The new entity. - */ - public static ConstraintConfigEntity fromPojo(final ConstraintConfig pojo) { - final ConstraintConfigEntity entity = new ConstraintConfigEntity(); - entity.update(pojo); - return entity; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/ConstraintEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ConstraintEntity.java deleted file mode 100644 index e621c93..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/ConstraintEntity.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.xnat.compute.models.Constraint; - -import javax.persistence.ElementCollection; -import javax.persistence.Entity; -import javax.persistence.MapsId; -import javax.persistence.OneToOne; -import java.util.HashSet; -import java.util.Set; - -@Entity -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode(callSuper = true) -public class ConstraintEntity extends AbstractHibernateEntity { - - private String key; - private Set constraintValues; // Different from model, values is a reserved word - private String operator; - - @ToString.Exclude @EqualsAndHashCode.Exclude private ConstraintConfigEntity constraintConfig; - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - @ElementCollection - public Set getConstraintValues() { - return constraintValues; - } - - public void setConstraintValues(Set constraintValues) { - if (constraintValues == null) { - constraintValues = new HashSet<>(); - } - - this.constraintValues = constraintValues; - } - - public String getOperator() { - return operator; - } - - public void setOperator(String operator) { - this.operator = operator; - } - - @OneToOne - @MapsId - public ConstraintConfigEntity getConstraintConfig() { - return constraintConfig; - } - - public void setConstraintConfig(ConstraintConfigEntity constraintConfig) { - this.constraintConfig = constraintConfig; - } - - public void update(final Constraint constraint) { - setKey(constraint.getKey()); - setConstraintValues(constraint.getValues()); - setOperator(constraint.getOperator().toString()); - setConstraintConfig(this.constraintConfig); - } - - public Constraint toPojo() { - return Constraint.builder() - .key(getKey()) - .values(getConstraintValues()) - .operator(Constraint.Operator.valueOf(getOperator())) - .build(); - } - - public static ConstraintEntity fromPojo(final Constraint constraint) { - final ConstraintEntity entity = new ConstraintEntity(); - entity.update(constraint); - return entity; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/ConstraintScopeEntity.java b/src/main/java/org/nrg/xnat/compute/entities/ConstraintScopeEntity.java deleted file mode 100644 index 05cd6b9..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/ConstraintScopeEntity.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.models.ConstraintScope; - -import javax.persistence.*; -import java.util.HashSet; -import java.util.Set; - -@Entity -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode -public class ConstraintScopeEntity { - - private long id; - private String scope; - private boolean enabled; - private Set ids; - - @ToString.Exclude @EqualsAndHashCode.Exclude private ConstraintConfigEntity constraintConfig; - - @Id - @GeneratedValue - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getScope() { - return scope; - } - - public void setScope(String scope) { - this.scope = scope; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @ElementCollection - public Set getIds() { - return ids; - } - - public void setIds(Set ids) { - this.ids = ids; - } - - @ManyToOne - public ConstraintConfigEntity getConstraintConfig() { - return constraintConfig; - } - - public void setConstraintConfig(ConstraintConfigEntity constraintConfig) { - this.constraintConfig = constraintConfig; - } - - /** - * Converts this entity to a pojo. - * @return The pojo. - */ - public ConstraintScope toPojo() { - return ConstraintScope.builder() - .scope(Scope.valueOf(getScope())) - .enabled(isEnabled()) - .ids(getIds()) - .build(); - } - - /** - * Updates this entity from the given pojo. - * @param pojo The pojo. - */ - public void update(final ConstraintScope pojo) { - setScope(pojo.getScope().name()); - setEnabled(pojo.isEnabled()); - - if (getIds() == null) { - // This is a new entity, so we need to initialize the collection - setIds(new HashSet<>()); - } else { - // This is an existing entity, so we need to clear the collection - getIds().clear(); - } - - // add new ids - if (pojo.getIds() != null) { - getIds().addAll(pojo.getIds()); - } - } - - /** - * Creates a new entity from the given pojo. - * @param pojo The pojo. - * @return The entity. - */ - public static ConstraintScopeEntity fromPojo(final ConstraintScope pojo) { - final ConstraintScopeEntity entity = new ConstraintScopeEntity(); - entity.update(pojo); - return entity; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/EnvironmentVariableEntity.java b/src/main/java/org/nrg/xnat/compute/entities/EnvironmentVariableEntity.java deleted file mode 100644 index 84f7dd4..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/EnvironmentVariableEntity.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.xnat.compute.models.EnvironmentVariable; - -import javax.persistence.Embeddable; - -@Embeddable -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode -public class EnvironmentVariableEntity { - - private String key; - private String value; - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public void update(EnvironmentVariable environmentVariable) { - setKey(environmentVariable.getKey()); - setValue(environmentVariable.getValue()); - } - - public EnvironmentVariable toPojo() { - return EnvironmentVariable.builder() - .key(key) - .value(value) - .build(); - } - - public static EnvironmentVariableEntity fromPojo(EnvironmentVariable environmentVariable) { - final EnvironmentVariableEntity entity = new EnvironmentVariableEntity(); - entity.update(environmentVariable); - return entity; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/GenericResourceEntity.java b/src/main/java/org/nrg/xnat/compute/entities/GenericResourceEntity.java deleted file mode 100644 index a147093..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/GenericResourceEntity.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.xnat.compute.models.GenericResource; - -import javax.persistence.Embeddable; - -@Embeddable -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode -public class GenericResourceEntity { - - private String name; - private String value; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public void update(GenericResource genericResource) { - setName(genericResource.getName()); - setValue(genericResource.getValue()); - } - - public GenericResource toPojo() { - return GenericResource.builder() - .name(name) - .value(value) - .build(); - } - - public static GenericResourceEntity fromPojo(GenericResource genericResource) { - final GenericResourceEntity entity = new GenericResourceEntity(); - entity.update(genericResource); - return entity; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/HardwareConfigEntity.java b/src/main/java/org/nrg/xnat/compute/entities/HardwareConfigEntity.java deleted file mode 100644 index 3c250a1..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/HardwareConfigEntity.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.framework.constants.Scope; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.xnat.compute.models.HardwareConfig; - -import javax.persistence.*; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Entity -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode(callSuper = true) -public class HardwareConfigEntity extends AbstractHibernateEntity { - - private HardwareEntity hardware; - private Map scopes; - - @ToString.Exclude @EqualsAndHashCode.Exclude private List computeEnvironmentHardwareOptions; - - @OneToOne(mappedBy = "hardwareConfig", cascade = CascadeType.ALL, orphanRemoval = true) - public HardwareEntity getHardware() { - return hardware; - } - - public void setHardware(HardwareEntity hardware) { - hardware.setHardwareConfig(this); - this.hardware = hardware; - } - - @OneToMany(mappedBy = "hardwareConfig", cascade = CascadeType.ALL, orphanRemoval = true) - public Map getScopes() { - return scopes; - } - - public void setScopes(Map scopes) { - this.scopes = scopes; - } - - @ManyToMany(mappedBy = "hardwareConfigs") - public List getComputeEnvironmentHardwareOptions() { - return computeEnvironmentHardwareOptions; - } - - public void setComputeEnvironmentHardwareOptions(List computeEnvironmentHardwareOptions) { - this.computeEnvironmentHardwareOptions = computeEnvironmentHardwareOptions; - } - - /** - * Creates a new entity from the given pojo. - * @param pojo The pojo from which to create the entity. - * @return The newly created entity. - */ - public static HardwareConfigEntity fromPojo(final HardwareConfig pojo) { - final HardwareConfigEntity entity = new HardwareConfigEntity(); - entity.update(pojo); - return entity; - } - - /** - * Creates a new pojo from the given entity. - * @return The newly created pojo. - */ - public HardwareConfig toPojo() { - return HardwareConfig.builder() - .id(getId()) - .hardware(getHardware().toPojo()) - .scopes(getScopes() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toPojo()))) - .build(); - } - - /** - * Updates the entity with the values from the given pojo. - * @param pojo The pojo from which to update the entity. - */ - public void update(final HardwareConfig pojo) { - if (getHardware() == null) { - // This is a new entity, so we need to create the hardware entity - setHardware(HardwareEntity.fromPojo(pojo.getHardware())); - } else { - // This is an existing entity, so we need to update the hardware entity - getHardware().update(pojo.getHardware()); - } - - // Set the hardware config on the hardware entity - getHardware().setHardwareConfig(this); - - if (getScopes() == null) { - // This is a new entity, so we need to create the scope entities - setScopes(pojo.getScopes() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> HardwareScopeEntity.fromPojo(e.getValue())))); - } else { - // This is an existing entity, so we need to update the scope entities - getScopes().forEach((key, value) -> value.update(pojo.getScopes().get(key))); - } - - // Set the hardware config on the scope entities - getScopes().forEach((key, value) -> value.setHardwareConfig(this)); - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/HardwareConstraintEntity.java b/src/main/java/org/nrg/xnat/compute/entities/HardwareConstraintEntity.java deleted file mode 100644 index 10a7c86..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/HardwareConstraintEntity.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.xnat.compute.models.Constraint; - -import javax.persistence.*; -import java.util.Set; - -@Entity -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -public class HardwareConstraintEntity extends AbstractHibernateEntity { - - private String key; - private Set constraintValues; // Different from model, values is a reserved word - private String operator; - - @ToString.Exclude @EqualsAndHashCode.Exclude private HardwareEntity hardware; - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - @ElementCollection(fetch = FetchType.EAGER) - public Set getConstraintValues() { - return constraintValues; - } - - public void setConstraintValues(Set constraintValues) { - this.constraintValues = constraintValues; - } - - public String getOperator() { - return operator; - } - - public void setOperator(String operator) { - this.operator = operator; - } - - @ManyToOne - @JoinColumn(name = "hardware_entity_id") - public HardwareEntity getHardware() { - return hardware; - } - - public void setHardware(HardwareEntity hardware) { - this.hardware = hardware; - } - - public void update(final Constraint constraint) { - setKey(constraint.getKey()); - setConstraintValues(constraint.getValues()); - setOperator(constraint.getOperator().toString()); - setHardware(this.hardware); - } - - public Constraint toPojo() { - return Constraint.builder() - .key(key) - .values(constraintValues) - .operator(Constraint.Operator.valueOf(operator)) - .build(); - } - - public static HardwareConstraintEntity fromPojo(final Constraint constraint) { - final HardwareConstraintEntity entity = new HardwareConstraintEntity(); - entity.update(constraint); - return entity; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/HardwareEntity.java b/src/main/java/org/nrg/xnat/compute/entities/HardwareEntity.java deleted file mode 100644 index 22ee0a7..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/HardwareEntity.java +++ /dev/null @@ -1,184 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; -import org.nrg.xnat.compute.models.Hardware; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -@Entity -@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"name"})}) -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode(callSuper = true) -public class HardwareEntity extends AbstractHibernateEntity { - - private String name; - - private Double cpuReservation; - private Double cpuLimit; - private String memoryReservation; - private String memoryLimit; - - private List constraints; - private List environmentVariables; - private List genericResources; - - @ToString.Exclude @EqualsAndHashCode.Exclude private HardwareConfigEntity hardwareConfig; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Double getCpuReservation() { - return cpuReservation; - } - - public void setCpuReservation(Double cpuReservation) { - this.cpuReservation = cpuReservation; - } - - public Double getCpuLimit() { - return cpuLimit; - } - - public void setCpuLimit(Double cpuLimit) { - this.cpuLimit = cpuLimit; - } - - public String getMemoryReservation() { - return memoryReservation; - } - - public void setMemoryReservation(String memoryReservation) { - this.memoryReservation = memoryReservation; - } - - public String getMemoryLimit() { - return memoryLimit; - } - - public void setMemoryLimit(String memoryLimit) { - this.memoryLimit = memoryLimit; - } - - @OneToMany(mappedBy = "hardware", cascade = CascadeType.ALL, orphanRemoval = true) - public List getConstraints() { - if (constraints == null) { - constraints = new ArrayList<>(); - } - - return constraints; - } - - public void setConstraints(List constraints) { - this.constraints = constraints; - } - - @ElementCollection - public List getEnvironmentVariables() { - if (environmentVariables == null) { - environmentVariables = new ArrayList<>(); - } - - return environmentVariables; - } - - public void setEnvironmentVariables(List environmentVariables) { - this.environmentVariables = environmentVariables; - } - - @ElementCollection - public List getGenericResources() { - if (genericResources == null) { - genericResources = new ArrayList<>(); - } - - return genericResources; - } - - public void setGenericResources(List genericResources) { - this.genericResources = genericResources; - } - - @OneToOne - public HardwareConfigEntity getHardwareConfig() { - return hardwareConfig; - } - - public void setHardwareConfig(HardwareConfigEntity hardwareConfig) { - this.hardwareConfig = hardwareConfig; - } - - public static HardwareEntity fromPojo(Hardware pojo) { - HardwareEntity entity = new HardwareEntity(); - entity.update(pojo); - return entity; - } - - public Hardware toPojo() { - return Hardware.builder() - .name(getName()) - .cpuReservation(getCpuReservation()) - .cpuLimit(getCpuLimit()) - .memoryReservation(getMemoryReservation()) - .memoryLimit(getMemoryLimit()) - .constraints(getConstraints().stream().map(HardwareConstraintEntity::toPojo).collect(Collectors.toList())) - .environmentVariables(getEnvironmentVariables().stream().map(EnvironmentVariableEntity::toPojo).collect(Collectors.toList())) - .genericResources(getGenericResources().stream().map(GenericResourceEntity::toPojo).collect(Collectors.toList())) - .build(); - } - - public void update(Hardware pojo) { - setName(pojo.getName()); - setCpuReservation(pojo.getCpuReservation()); - setCpuLimit(pojo.getCpuLimit()); - setMemoryReservation(pojo.getMemoryReservation()); - setMemoryLimit(pojo.getMemoryLimit()); - - // remove old constraints, need to remove hardware reference from old constraints before clearing - getConstraints().forEach(c -> c.setHardware(null)); - getConstraints().clear(); - - // add new constraints - if (pojo.getConstraints() != null) { - getConstraints().addAll(pojo.getConstraints() - .stream() - .map(HardwareConstraintEntity::fromPojo) - .collect(Collectors.toList())); - getConstraints().forEach(c -> c.setHardware(this)); - } - - // remove old environment variables - getEnvironmentVariables().clear(); - - // add new environment variables - if (pojo.getEnvironmentVariables() != null) { - getEnvironmentVariables().addAll(pojo.getEnvironmentVariables() - .stream() - .map(EnvironmentVariableEntity::fromPojo) - .collect(Collectors.toList())); - } - - // remove old resources - getGenericResources().clear(); - - // add new resources - if (pojo.getGenericResources() != null) { - getGenericResources().addAll(pojo.getGenericResources() - .stream() - .map(GenericResourceEntity::fromPojo) - .collect(Collectors.toList())); - } - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/HardwareScopeEntity.java b/src/main/java/org/nrg/xnat/compute/entities/HardwareScopeEntity.java deleted file mode 100644 index 47c3e2d..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/HardwareScopeEntity.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.models.HardwareScope; - -import javax.persistence.*; -import java.util.HashSet; -import java.util.Set; - -@Entity -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode -public class HardwareScopeEntity { - - private long id; - private String scope; - private boolean enabled; - private Set ids; - - @ToString.Exclude @EqualsAndHashCode.Exclude private HardwareConfigEntity hardwareConfig; - - @Id - @GeneratedValue - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public boolean isEnabled() { - return enabled; - } - - public String getScope() { - return scope; - } - - public void setScope(String scope) { - this.scope = scope; - } - - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @ElementCollection - public Set getIds() { - return ids; - } - - public void setIds(Set ids) { - this.ids = ids; - } - - @ManyToOne - public HardwareConfigEntity getHardwareConfig() { - return hardwareConfig; - } - - public void setHardwareConfig(HardwareConfigEntity hardwareConfig) { - this.hardwareConfig = hardwareConfig; - } - - /** - * Converts this entity to a pojo. - * @return The pojo. - */ - public HardwareScope toPojo() { - return HardwareScope.builder() - .scope(Scope.valueOf(getScope())) - .enabled(isEnabled()) - .ids(getIds()) - .build(); - } - - /** - * Updates this entity from the given pojo. - * @param pojo The pojo. - */ - public void update(final HardwareScope pojo) { - setScope(pojo.getScope().name()); - setEnabled(pojo.isEnabled()); - - if (getIds() == null) { - // This is a new entity, so we need to initialize the collection - setIds(new HashSet<>()); - } else { - // This is an existing entity, so we need to clear the collection - getIds().clear(); - } - - // add new ids - if (pojo.getIds() != null) { - getIds().addAll(pojo.getIds()); - } - } - - /** - * Creates a new entity from the given pojo. - * @param pojo The pojo. - * @return The entity. - */ - public static HardwareScopeEntity fromPojo(final HardwareScope pojo) { - final HardwareScopeEntity entity = new HardwareScopeEntity(); - entity.update(pojo); - return entity; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/entities/MountEntity.java b/src/main/java/org/nrg/xnat/compute/entities/MountEntity.java deleted file mode 100644 index 2c5d9f2..0000000 --- a/src/main/java/org/nrg/xnat/compute/entities/MountEntity.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.nrg.xnat.compute.entities; - -import lombok.*; -import org.nrg.xnat.compute.models.Mount; - -import javax.persistence.Embeddable; - -@Embeddable -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode -public class MountEntity { - - private String volumeName; - private String localPath; - private String containerPath; - private boolean readOnly; - - public String getVolumeName() { - return volumeName; - } - - public void setVolumeName(String volumeName) { - this.volumeName = volumeName; - } - - public String getLocalPath() { - return localPath; - } - - public void setLocalPath(String localPath) { - this.localPath = localPath; - } - - public String getContainerPath() { - return containerPath; - } - - public void setContainerPath(String containerPath) { - this.containerPath = containerPath; - } - - public boolean isReadOnly() { - return readOnly; - } - - public void setReadOnly(boolean readOnly) { - this.readOnly = readOnly; - } - - public void update(Mount mount) { - setVolumeName(mount.getVolumeName()); - setLocalPath(mount.getLocalPath()); - setContainerPath(mount.getContainerPath()); - setReadOnly(mount.isReadOnly()); - } - - public Mount toPojo() { - return Mount.builder() - .volumeName(volumeName) - .localPath(localPath) - .containerPath(containerPath) - .readOnly(readOnly) - .build(); - } - - public static MountEntity fromPojo(Mount mount) { - final MountEntity entity = new MountEntity(); - entity.update(mount); - return entity; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/AccessScope.java b/src/main/java/org/nrg/xnat/compute/models/AccessScope.java deleted file mode 100644 index 09d5405..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/AccessScope.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.nrg.xnat.compute.models; - -import org.nrg.framework.constants.Scope; - -import java.util.Map; -import java.util.Set; - -public interface AccessScope { - - Scope getScope(); - - boolean isEnabled(); // is enabled for all ids - - Set getIds(); // ids that are enabled for this scope (if isEnabled is false) - - /** - * A scope/id combination is enabled if the scope is enabled for all ids or if the id is in the set of ids - * - * @param id The id to check - * @return Whether the scope/id combination is enabled - */ - default boolean isEnabledFor(String id) { - if (isEnabled()) { - return true; - } else { - return getIds().contains(id); - } - } - - /** - * Given the provided user execution scope, determine if the config is enabled for - * - * @param configRequiredAccessScopes The required access scopes for the config - * @param userExecutionScope The user's execution scope - * @return Whether the config is enabled for the provided execution scope - */ - static boolean isEnabledFor(Map configRequiredAccessScopes, Map userExecutionScope) { - for (Scope scope : Scope.values()) { - if (configRequiredAccessScopes.containsKey(scope)) { - AccessScope configRequiredAccessScope = configRequiredAccessScopes.get(scope); - if (!configRequiredAccessScope.isEnabledFor(userExecutionScope.get(scope))) { - return false; - } - } - } - return true; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironment.java b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironment.java deleted file mode 100644 index ae4145b..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironment.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.nrg.xnat.compute.models; - -import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class ComputeEnvironment { - - @ApiModelProperty(position = 0) private String name; - @ApiModelProperty(position = 1) private String image; - @ApiModelProperty(position = 2) private String command; - @ApiModelProperty(position = 3) private List environmentVariables; - @ApiModelProperty(position = 4) private List mounts; - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentConfig.java b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentConfig.java deleted file mode 100644 index c92a681..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentConfig.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.nrg.xnat.compute.models; - -import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.nrg.framework.constants.Scope; - -import java.util.Map; -import java.util.Set; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class ComputeEnvironmentConfig { - - @ApiModelProperty(position = 0) private Long id; - @ApiModelProperty(position = 1) private Set configTypes; - @ApiModelProperty(position = 2) private ComputeEnvironment computeEnvironment; - @ApiModelProperty(position = 3) private Map scopes; - @ApiModelProperty(position = 4) private ComputeEnvironmentHardwareOptions hardwareOptions; - - public enum ConfigType { - JUPYTERHUB, - CONTAINER_SERVICE, - GENERAL - } -} diff --git a/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentHardwareOptions.java b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentHardwareOptions.java deleted file mode 100644 index adf57fe..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentHardwareOptions.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.nrg.xnat.compute.models; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.Set; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class ComputeEnvironmentHardwareOptions { - - private boolean allowAllHardware; - private Set hardwareConfigs; - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentScope.java b/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentScope.java deleted file mode 100644 index a1833ef..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/ComputeEnvironmentScope.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.nrg.xnat.compute.models; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.nrg.framework.constants.Scope; - -import java.util.Set; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class ComputeEnvironmentScope implements AccessScope { - - private Scope scope; - private boolean enabled; // Whether the scope is enabled for all ids or only for the ids in the set - private Set ids; // The set of ids that are enabled for this scope (if isEnabled is false) - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/Constraint.java b/src/main/java/org/nrg/xnat/compute/models/Constraint.java deleted file mode 100644 index 1e27938..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/Constraint.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.nrg.xnat.compute.models; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class Constraint { - - public enum Operator { - IN, - NOT_IN, - } - - private String key; - private Set values; - private Operator operator; - - /** - * Convert to list of Docker constraint strings. Example: ["key==value1", "key==value2"] - * @return List of Docker constraint strings - */ - public List toList() { - List list = new ArrayList<>(); - - values.forEach((value) -> { - final String operatorString; - - switch (operator) { - case IN: - operatorString = "=="; - break; - case NOT_IN: - operatorString = "!="; - break; - default: - throw new RuntimeException("Unknown constraint operator: " + operator); - } - - list.add(key + operatorString + value); - }); - - return list; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/ConstraintConfig.java b/src/main/java/org/nrg/xnat/compute/models/ConstraintConfig.java deleted file mode 100644 index 9fbcbe2..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/ConstraintConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.nrg.xnat.compute.models; - -import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.nrg.framework.constants.Scope; - -import java.util.Map; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class ConstraintConfig { - - @ApiModelProperty(position = 0) private Long id; - @ApiModelProperty(position = 1) private Constraint constraint; - @ApiModelProperty(position = 2) private Map scopes; - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/ConstraintScope.java b/src/main/java/org/nrg/xnat/compute/models/ConstraintScope.java deleted file mode 100644 index fc9678f..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/ConstraintScope.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.nrg.xnat.compute.models; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.nrg.framework.constants.Scope; - -import java.util.Set; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class ConstraintScope implements AccessScope { - - private Scope scope; - private boolean enabled; // Whether the scope is enabled for all ids or only for the ids in the set - private Set ids; // The set of ids that are enabled for this scope (if isEnabled is false) - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/EnvironmentVariable.java b/src/main/java/org/nrg/xnat/compute/models/EnvironmentVariable.java deleted file mode 100644 index d605fa6..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/EnvironmentVariable.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.nrg.xnat.compute.models; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class EnvironmentVariable { - - private String key; - private String value; - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/GenericResource.java b/src/main/java/org/nrg/xnat/compute/models/GenericResource.java deleted file mode 100644 index c2a6995..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/GenericResource.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.nrg.xnat.compute.models; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class GenericResource { - - private String name; - private String value; - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/Hardware.java b/src/main/java/org/nrg/xnat/compute/models/Hardware.java deleted file mode 100644 index a68e442..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/Hardware.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.nrg.xnat.compute.models; - -import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class Hardware { - - @ApiModelProperty(position = 0) private String name; - @ApiModelProperty(position = 1) private Double cpuLimit; - @ApiModelProperty(position = 2) private Double cpuReservation; - @ApiModelProperty(position = 3) private String memoryLimit; - @ApiModelProperty(position = 4) private String memoryReservation; - @ApiModelProperty(position = 5) private List constraints; - @ApiModelProperty(position = 6) private List environmentVariables; - @ApiModelProperty(position = 7) private List genericResources; - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/HardwareConfig.java b/src/main/java/org/nrg/xnat/compute/models/HardwareConfig.java deleted file mode 100644 index c9eaf68..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/HardwareConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.nrg.xnat.compute.models; - -import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.nrg.framework.constants.Scope; - -import java.util.Map; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class HardwareConfig { - - @ApiModelProperty(position = 0) private Long id; - @ApiModelProperty(position = 1) private Hardware hardware; - @ApiModelProperty(position = 2) private Map scopes; - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/HardwareScope.java b/src/main/java/org/nrg/xnat/compute/models/HardwareScope.java deleted file mode 100644 index 288c767..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/HardwareScope.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.nrg.xnat.compute.models; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.nrg.framework.constants.Scope; - -import java.util.Set; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class HardwareScope implements AccessScope { - - private Scope scope; - private boolean enabled; // Whether the scope is enabled for all ids or only for the ids in the set - private Set ids; // The set of ids that are enabled for this scope (if isEnabled is false) - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/JobTemplate.java b/src/main/java/org/nrg/xnat/compute/models/JobTemplate.java deleted file mode 100644 index 85eee12..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/JobTemplate.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.nrg.xnat.compute.models; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class JobTemplate { - - private ComputeEnvironment computeEnvironment; - private Hardware hardware; - private List constraints; - -} diff --git a/src/main/java/org/nrg/xnat/compute/models/Mount.java b/src/main/java/org/nrg/xnat/compute/models/Mount.java deleted file mode 100644 index 8c3c15c..0000000 --- a/src/main/java/org/nrg/xnat/compute/models/Mount.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.nrg.xnat.compute.models; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class Mount { - - private String volumeName; - private String localPath; - private String containerPath; - private boolean readOnly; - -} diff --git a/src/main/java/org/nrg/xnat/compute/repositories/ComputeEnvironmentConfigDao.java b/src/main/java/org/nrg/xnat/compute/repositories/ComputeEnvironmentConfigDao.java deleted file mode 100644 index 21c9de0..0000000 --- a/src/main/java/org/nrg/xnat/compute/repositories/ComputeEnvironmentConfigDao.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.nrg.xnat.compute.repositories; - -import lombok.extern.slf4j.Slf4j; -import org.hibernate.Criteria; -import org.hibernate.Hibernate; -import org.hibernate.SessionFactory; -import org.hibernate.criterion.Restrictions; -import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; -import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; -import org.nrg.xnat.compute.entities.ComputeEnvironmentScopeEntity; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -import java.util.Collections; -import java.util.List; - -@Repository -@Slf4j -public class ComputeEnvironmentConfigDao extends AbstractHibernateDAO { - - private final HardwareConfigDao hardwareConfigDao; - - // For testing - public ComputeEnvironmentConfigDao(final SessionFactory sessionFactory, - final HardwareConfigDao hardwareConfigDao) { - super(sessionFactory); - this.hardwareConfigDao = hardwareConfigDao; - } - - @Autowired - public ComputeEnvironmentConfigDao(HardwareConfigDao hardwareConfigDao) { - this.hardwareConfigDao = hardwareConfigDao; - } - - /** - * Initializes the entity, loading all collections and proxies. - * @param entity The entity to initialize. - */ - @Override - public void initialize(final ComputeEnvironmentConfigEntity entity) { - if (entity == null) { - return; - } - - Hibernate.initialize(entity); - - Hibernate.initialize(entity.getConfigTypes()); - - Hibernate.initialize(entity.getComputeEnvironment()); - if (entity.getComputeEnvironment() != null) { - Hibernate.initialize(entity.getComputeEnvironment().getEnvironmentVariables()); - Hibernate.initialize(entity.getComputeEnvironment().getMounts()); - } - - Hibernate.initialize(entity.getScopes()); - if (entity.getScopes() != null) { - entity.getScopes().forEach((scope, computeEnvironmentScopeEntity) -> { - initialize(computeEnvironmentScopeEntity); - }); - } - - Hibernate.initialize(entity.getHardwareOptions()); - if (entity.getHardwareOptions() != null) { - Hibernate.initialize(entity.getHardwareOptions().getHardwareConfigs()); - - if (entity.getHardwareOptions().getHardwareConfigs() != null) { - for (HardwareConfigEntity hardwareConfig : entity.getHardwareOptions().getHardwareConfigs()) { - hardwareConfigDao.initialize(hardwareConfig); - } - } - } - } - - /** - * Initializes the entity, loading all collections and proxies. - * @param entity The entity to initialize. - */ - public void initialize(ComputeEnvironmentScopeEntity entity) { - if (entity == null) { - return; - } - - Hibernate.initialize(entity); - Hibernate.initialize(entity.getIds()); - } - - /** - * Finds all compute environment configs that have the specified type. - * @param type The type to search for. - * @return The list of compute environment configs that have the specified type. - */ - public List findByType(String type) { - // Need to use a criteria query because the configTypes field is a collection. - Criteria criteria = getSession().createCriteria(ComputeEnvironmentConfigEntity.class) - .createCriteria("configTypes") - .add(Restrictions.eq("elements", type)); - - List entities = criteria.list(); - - if (entities == null) { - return Collections.emptyList(); - } - - for (ComputeEnvironmentConfigEntity entity : entities) { - initialize(entity); - } - - return entities; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/repositories/ConstraintConfigDao.java b/src/main/java/org/nrg/xnat/compute/repositories/ConstraintConfigDao.java deleted file mode 100644 index 46ccb97..0000000 --- a/src/main/java/org/nrg/xnat/compute/repositories/ConstraintConfigDao.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.nrg.xnat.compute.repositories; - -import lombok.extern.slf4j.Slf4j; -import org.hibernate.Hibernate; -import org.hibernate.SessionFactory; -import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; -import org.nrg.xnat.compute.entities.ConstraintConfigEntity; -import org.nrg.xnat.compute.entities.ConstraintEntity; -import org.nrg.xnat.compute.entities.ConstraintScopeEntity; -import org.springframework.stereotype.Repository; - -@Repository -@Slf4j -public class ConstraintConfigDao extends AbstractHibernateDAO { - - // For testing - public ConstraintConfigDao(final SessionFactory sessionFactory) { - super(sessionFactory); - } - - /** - * Initialize the constraint config entity. - * @param entity The entity to initialize. - */ - @Override - public void initialize(final ConstraintConfigEntity entity) { - if (entity == null) { - return; - } - - initialize(entity.getConstraint()); - - Hibernate.initialize(entity.getScopes()); - if (entity.getScopes() != null) { - entity.getScopes().forEach((scope, constraintScopeEntity) -> { - initialize(constraintScopeEntity); - }); - } - } - - /** - * Initialize the constraint entity. - * @param entity The entity to initialize. - */ - void initialize(final ConstraintEntity entity) { - if (entity == null) { - return; - } - - Hibernate.initialize(entity); - if (entity.getConstraintValues() != null) { - Hibernate.initialize(entity.getConstraintValues()); - } - } - - /** - * Initialize the constraint scope entity. - * @param entity The entity to initialize. - */ - public void initialize(ConstraintScopeEntity entity) { - if (entity == null) { - return; - } - - Hibernate.initialize(entity); - Hibernate.initialize(entity.getIds()); - } -} diff --git a/src/main/java/org/nrg/xnat/compute/repositories/HardwareConfigDao.java b/src/main/java/org/nrg/xnat/compute/repositories/HardwareConfigDao.java deleted file mode 100644 index b9ef99c..0000000 --- a/src/main/java/org/nrg/xnat/compute/repositories/HardwareConfigDao.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.nrg.xnat.compute.repositories; - -import lombok.extern.slf4j.Slf4j; -import org.hibernate.Hibernate; -import org.hibernate.SessionFactory; -import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.entities.HardwareConstraintEntity; -import org.nrg.xnat.compute.entities.HardwareEntity; -import org.nrg.xnat.compute.entities.HardwareScopeEntity; -import org.springframework.stereotype.Repository; - -@Repository -@Slf4j -public class HardwareConfigDao extends AbstractHibernateDAO { - - // For testing - public HardwareConfigDao(final SessionFactory sessionFactory) { - super(sessionFactory); - } - - /** - * Initializes the entity, loading all collections and proxies. - * @param entity The entity to initialize. - */ - @Override - public void initialize(final HardwareConfigEntity entity) { - if (entity == null) { - return; - } - - Hibernate.initialize(entity); - - Hibernate.initialize(entity.getScopes()); - if (entity.getScopes() != null) { - entity.getScopes().forEach((scope, hardwareScopeEntity) -> { - initialize(hardwareScopeEntity); - }); - } - - Hibernate.initialize(entity.getHardware()); - if (entity.getHardware() != null) { - initialize(entity.getHardware()); - } - - if (entity.getComputeEnvironmentHardwareOptions() != null) { - Hibernate.initialize(entity.getComputeEnvironmentHardwareOptions()); - } - } - - /** - * Initializes the entity, loading all collections and proxies. - * @param entity The entity to initialize. - */ - public void initialize(HardwareScopeEntity entity) { - if (entity == null) { - return; - } - - Hibernate.initialize(entity); - Hibernate.initialize(entity.getIds()); - } - - /** - * Initializes the entity, loading all collections and proxies. - * @param entity The entity to initialize. - */ - public void initialize(final HardwareEntity entity) { - if (entity == null) { - return; - } - - Hibernate.initialize(entity); - - Hibernate.initialize(entity.getConstraints()); - if (entity.getConstraints() != null) { - for (final HardwareConstraintEntity constraint : entity.getConstraints()) { - Hibernate.initialize(constraint.getConstraintValues()); - } - } - - Hibernate.initialize(entity.getEnvironmentVariables()); - Hibernate.initialize(entity.getGenericResources()); - } -} diff --git a/src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java b/src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java deleted file mode 100644 index 2f567b1..0000000 --- a/src/main/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApi.java +++ /dev/null @@ -1,143 +0,0 @@ -package org.nrg.xnat.compute.rest; - -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import org.nrg.framework.annotations.XapiRestController; -import org.nrg.framework.constants.Scope; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xapi.rest.AbstractXapiRestController; -import org.nrg.xapi.rest.XapiRequestMapping; -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static org.nrg.xdat.security.helpers.AccessLevel.Admin; -import static org.nrg.xdat.security.helpers.AccessLevel.Read; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; - -@Api("ComputeEnvironmentConfigs REST API") -@XapiRestController -@RequestMapping(value = "/compute-environment-configs") -public class ComputeEnvironmentConfigsApi extends AbstractXapiRestController { - - private final ComputeEnvironmentConfigService computeEnvironmentConfigService; - - @Autowired - public ComputeEnvironmentConfigsApi(final UserManagementServiceI userManagementService, - final RoleHolder roleHolder, - final ComputeEnvironmentConfigService computeEnvironmentConfigService) { - super(userManagementService, roleHolder); - this.computeEnvironmentConfigService = computeEnvironmentConfigService; - } - - @ApiOperation(value = "Get all compute environment configs or all compute environment configs for a given type.", response = ComputeEnvironmentConfig.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Compute environment configs successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Admin) - public List getAll(@RequestParam(value = "type", required = false) final ComputeEnvironmentConfig.ConfigType type) { - if (type != null) { - return computeEnvironmentConfigService.getByType(type); - } else { - return computeEnvironmentConfigService.getAll(); - } - } - - @ApiOperation(value = "Get a compute environment config.", response = ComputeEnvironmentConfig.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Compute environment config successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Compute environment config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Admin) - public ComputeEnvironmentConfig get(@PathVariable("id") final Long id) throws NotFoundException { - return computeEnvironmentConfigService.retrieve(id) - .orElseThrow(() -> new NotFoundException("Compute environment config not found.")); - } - - @ApiOperation(value = "Create a compute environment config.", response = ComputeEnvironmentConfig.class) - @ApiResponses({ - @ApiResponse(code = 201, message = "Compute environment config successfully created."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @ResponseStatus(HttpStatus.CREATED) - @XapiRequestMapping(value = "", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.POST, restrictTo = Admin) - public ComputeEnvironmentConfig create(@RequestBody final ComputeEnvironmentConfig computeEnvironmentConfig) { - return computeEnvironmentConfigService.create(computeEnvironmentConfig); - } - - @ApiOperation(value = "Update a compute environment config.", response = ComputeEnvironmentConfig.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Compute environment config successfully updated."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Compute environment config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/{id}", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = RequestMethod.PUT, restrictTo = Admin) - public ComputeEnvironmentConfig update(@PathVariable("id") final Long id, - @RequestBody final ComputeEnvironmentConfig computeEnvironmentConfig) throws NotFoundException { - if (!id.equals(computeEnvironmentConfig.getId())) { - throw new IllegalArgumentException("The ID in the path must match the ID in the body."); - } - - return computeEnvironmentConfigService.update(computeEnvironmentConfig); - } - - @ApiOperation(value = "Delete a compute environment config.") - @ApiResponses({ - @ApiResponse(code = 204, message = "Compute environment config successfully deleted."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Compute environment config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @ResponseStatus(HttpStatus.NO_CONTENT) - @XapiRequestMapping(value = "/{id}", method = RequestMethod.DELETE, restrictTo = Admin) - public void delete(@PathVariable("id") final Long id) throws NotFoundException { - computeEnvironmentConfigService.delete(id); - } - - @ApiOperation(value = "Get all available compute environment configs for the provided execution scope.", response = ComputeEnvironmentConfig.class, responseContainer = "List") - @ApiResponses({ - @ApiResponse(code = 200, message = "Compute environment configs successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/available", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Read) - public List getAvailable(@RequestParam final Map params) { - ComputeEnvironmentConfig.ConfigType type = null; - - // If the type is specified, remove it from the params map so it doesn't get used as an execution scope. - if (params.containsKey("type")) { - type = ComputeEnvironmentConfig.ConfigType.valueOf(params.get("type")); - params.remove("type"); - } - - // Get the execution scope from the params map. - Map executionScope = params.entrySet().stream() - .filter(entry -> Scope.getCodes().contains(entry.getKey())) - .collect(Collectors.toMap(entry -> Scope.getScope(entry.getKey()), Map.Entry::getValue)); - - return computeEnvironmentConfigService.getAvailable(type, executionScope); - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/rest/ConstraintConfigsApi.java b/src/main/java/org/nrg/xnat/compute/rest/ConstraintConfigsApi.java deleted file mode 100644 index 50b711d..0000000 --- a/src/main/java/org/nrg/xnat/compute/rest/ConstraintConfigsApi.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.nrg.xnat.compute.rest; - -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import org.nrg.framework.annotations.XapiRestController; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xapi.rest.AbstractXapiRestController; -import org.nrg.xapi.rest.XapiRequestMapping; -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xnat.compute.models.ConstraintConfig; -import org.nrg.xnat.compute.services.ConstraintConfigService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; - -import java.util.List; - -import static org.nrg.xdat.security.helpers.AccessLevel.Admin; -import static org.springframework.http.HttpStatus.CREATED; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import static org.springframework.web.bind.annotation.RequestMethod.*; - -@Api("Constraint Configs REST API") -@XapiRestController -@RequestMapping(value = "/constraint-configs") -public class ConstraintConfigsApi extends AbstractXapiRestController { - - private final ConstraintConfigService constraintConfigService; - - @Autowired - public ConstraintConfigsApi(final UserManagementServiceI userManagementService, - final RoleHolder roleHolder, - final ConstraintConfigService constraintConfigService) { - super(userManagementService, roleHolder); - this.constraintConfigService = constraintConfigService; - } - - @ApiOperation(value = "Get a constraint config.", response = ConstraintConfig.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Placement constraint config successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Placement constraint config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Admin) - public ConstraintConfig get(@PathVariable final Long id) throws NotFoundException { - return constraintConfigService.retrieve(id) - .orElseThrow(() -> new NotFoundException("No placement constraint config found with ID " + id)); - } - - @ApiOperation(value = "Get all constraint configs.", response = ConstraintConfig.class, responseContainer = "List") - @ApiResponses({ - @ApiResponse(code = 200, message = "Placement constraint configs successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Admin) - public List getAll() { - return constraintConfigService.getAll(); - } - - @ApiOperation(value = "Create a constraint config.", response = ConstraintConfig.class) - @ApiResponses({ - @ApiResponse(code = 201, message = "Placement constraint config successfully created."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @ResponseStatus(CREATED) - @XapiRequestMapping(value = "", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = POST, restrictTo = Admin) - public ConstraintConfig create(@RequestBody final ConstraintConfig constraintConfig) { - return constraintConfigService.create(constraintConfig); - } - - @ApiOperation(value = "Update a constraint config.", response = ConstraintConfig.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Placement constraint config successfully updated."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Placement constraint config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/{id}", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = PUT, restrictTo = Admin) - public ConstraintConfig update(@PathVariable final Long id, - @RequestBody final ConstraintConfig constraintConfig) throws NotFoundException { - if (!id.equals(constraintConfig.getId())) { - throw new IllegalArgumentException("Placement constraint config ID in path does not match ID in body."); - } - - return constraintConfigService.update(constraintConfig); - } - - @ApiOperation(value = "Delete a constraint config.") - @ApiResponses({ - @ApiResponse(code = 204, message = "Placement constraint config successfully deleted."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Placement constraint config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @ResponseStatus(HttpStatus.NO_CONTENT) - @XapiRequestMapping(value = "/{id}", method = DELETE, restrictTo = Admin) - public void delete(@PathVariable final Long id) throws NotFoundException { - constraintConfigService.delete(id); - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/rest/HardwareConfigsApi.java b/src/main/java/org/nrg/xnat/compute/rest/HardwareConfigsApi.java deleted file mode 100644 index 60af77e..0000000 --- a/src/main/java/org/nrg/xnat/compute/rest/HardwareConfigsApi.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.nrg.xnat.compute.rest; - -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import org.nrg.framework.annotations.XapiRestController; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xapi.rest.AbstractXapiRestController; -import org.nrg.xapi.rest.XapiRequestMapping; -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xnat.compute.models.HardwareConfig; -import org.nrg.xnat.compute.services.HardwareConfigService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; - -import java.util.List; - -import static org.nrg.xdat.security.helpers.AccessLevel.Admin; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import static org.springframework.web.bind.annotation.RequestMethod.*; - -@Api("Hardware Configs REST API") -@XapiRestController -@RequestMapping(value = "/hardware-configs") -public class HardwareConfigsApi extends AbstractXapiRestController { - - private final HardwareConfigService hardwareConfigService; - - @Autowired - public HardwareConfigsApi(final UserManagementServiceI userManagementService, - final RoleHolder roleHolder, - final HardwareConfigService hardwareConfigService) { - super(userManagementService, roleHolder); - this.hardwareConfigService = hardwareConfigService; - } - - @ApiOperation(value = "Get a hardware config.", response = HardwareConfig.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Hardware config successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Hardware config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Admin) - public HardwareConfig get(@PathVariable("id") final Long id) throws NotFoundException { - return hardwareConfigService.retrieve(id).orElseThrow(() -> new NotFoundException("No hardware config found for ID " + id)); - } - - @ApiOperation(value = "Get all hardware configs.", response = HardwareConfig.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Hardware configs successfully retrieved."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Admin) - public List getAll() { - return hardwareConfigService.retrieveAll(); - } - - @ApiOperation(value = "Create a hardware config.", response = HardwareConfig.class) - @ApiResponses({ - @ApiResponse(code = 201, message = "Hardware config successfully created."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @ResponseStatus(HttpStatus.CREATED) - @XapiRequestMapping(value = "", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE, method = POST, restrictTo = Admin) - public HardwareConfig create(@RequestBody final HardwareConfig hardwareConfig) { - return hardwareConfigService.create(hardwareConfig); - } - - @ApiOperation(value = "Update a hardware config.", response = HardwareConfig.class) - @ApiResponses({ - @ApiResponse(code = 200, message = "Hardware config successfully updated."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Hardware config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @XapiRequestMapping(value = "/{id}", consumes = APPLICATION_JSON_VALUE, method = PUT, restrictTo = Admin) - public HardwareConfig update(@PathVariable("id") final Long id, - @RequestBody final HardwareConfig hardwareConfig) throws NotFoundException { - if (!id.equals(hardwareConfig.getId())) { - throw new IllegalArgumentException("The hardware config ID in the path must match the ID in the body."); - } - return hardwareConfigService.update(hardwareConfig); - } - - @ApiOperation(value = "Delete a hardware config.") - @ApiResponses({ - @ApiResponse(code = 204, message = "Hardware config successfully deleted."), - @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), - @ApiResponse(code = 403, message = "Not authorized."), - @ApiResponse(code = 404, message = "Hardware config not found."), - @ApiResponse(code = 500, message = "Unexpected error") - }) - @ResponseStatus(HttpStatus.NO_CONTENT) - @XapiRequestMapping(value = "/{id}", method = DELETE, restrictTo = Admin) - public void delete(@PathVariable("id") final Long id) throws NotFoundException { - hardwareConfigService.delete(id); - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigEntityService.java deleted file mode 100644 index 9e19830..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigEntityService.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.nrg.xnat.compute.services; - -import org.nrg.framework.orm.hibernate.BaseHibernateService; -import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; -import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; - -import java.util.List; - -public interface ComputeEnvironmentConfigEntityService extends BaseHibernateService { - - void addHardwareConfigEntity(Long computeEnvironmentConfigId, Long hardwareConfigId); - void removeHardwareConfigEntity(Long computeEnvironmentConfigId, Long hardwareConfigId); - List findByType(ComputeEnvironmentConfig.ConfigType type); - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java b/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java deleted file mode 100644 index 829b8eb..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/ComputeEnvironmentConfigService.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.nrg.xnat.compute.services; - -import org.nrg.framework.constants.Scope; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -public interface ComputeEnvironmentConfigService { - - boolean exists(Long id); - Optional retrieve(Long id); - List getAll(); - List getByType(ComputeEnvironmentConfig.ConfigType type); - List getAvailable(Map executionScope); - List getAvailable(ComputeEnvironmentConfig.ConfigType type, Map executionScope); - boolean isAvailable(Long id, Map executionScope); - ComputeEnvironmentConfig create(ComputeEnvironmentConfig computeEnvironmentConfig); - ComputeEnvironmentConfig update(ComputeEnvironmentConfig computeEnvironmentConfig) throws NotFoundException; - void delete(Long id) throws NotFoundException; - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigEntityService.java deleted file mode 100644 index 4adcd2b..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigEntityService.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.nrg.xnat.compute.services; - -import org.nrg.framework.orm.hibernate.BaseHibernateService; -import org.nrg.xnat.compute.entities.ConstraintConfigEntity; - -public interface ConstraintConfigEntityService extends BaseHibernateService { - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigService.java b/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigService.java deleted file mode 100644 index fe0c8f2..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/ConstraintConfigService.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.nrg.xnat.compute.services; - -import org.nrg.framework.constants.Scope; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xnat.compute.models.ConstraintConfig; - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -public interface ConstraintConfigService { - - Optional retrieve(Long id); - List getAll(); - List getAvailable(Map executionScope); - ConstraintConfig create(ConstraintConfig config); - ConstraintConfig update(ConstraintConfig config) throws NotFoundException; - void delete(Long id); - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/HardwareConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/HardwareConfigEntityService.java deleted file mode 100644 index de8aa74..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/HardwareConfigEntityService.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.nrg.xnat.compute.services; - -import org.nrg.framework.orm.hibernate.BaseHibernateService; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; - -public interface HardwareConfigEntityService extends BaseHibernateService { - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/HardwareConfigService.java b/src/main/java/org/nrg/xnat/compute/services/HardwareConfigService.java deleted file mode 100644 index 64b0b9a..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/HardwareConfigService.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.nrg.xnat.compute.services; - -import org.nrg.framework.constants.Scope; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xnat.compute.models.HardwareConfig; - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -public interface HardwareConfigService { - - boolean exists(Long id); - Optional retrieve(Long id); - List retrieveAll(); - HardwareConfig create(HardwareConfig hardwareConfig); - HardwareConfig update(HardwareConfig hardwareConfig) throws NotFoundException; - void delete(Long id) throws NotFoundException; - boolean isAvailable(Long id, Map executionScope); - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java b/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java deleted file mode 100644 index 9e1f3da..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/JobTemplateService.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.nrg.xnat.compute.services; - -import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.models.JobTemplate; - -import java.util.Map; - -public interface JobTemplateService { - - boolean isAvailable(Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope); - JobTemplate resolve(Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope); - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java deleted file mode 100644 index 048ce91..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigService.java +++ /dev/null @@ -1,389 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.nrg.framework.constants.Scope; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.models.*; -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; -import org.nrg.xnat.compute.services.HardwareConfigEntityService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.*; -import java.util.stream.Collectors; - -@Service -@Slf4j -public class DefaultComputeEnvironmentConfigService implements ComputeEnvironmentConfigService { - - private final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService; - private final HardwareConfigEntityService hardwareConfigEntityService; - - @Autowired - public DefaultComputeEnvironmentConfigService(final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService, - final HardwareConfigEntityService hardwareConfigEntityService) { - this.computeEnvironmentConfigEntityService = computeEnvironmentConfigEntityService; - this.hardwareConfigEntityService = hardwareConfigEntityService; - } - - /** - * Checks if a ComputeEnvironmentConfig with the given ID exists. - * - * @param id The ID of the ComputeEnvironmentConfig to check for. - * @return True if a ComputeEnvironmentConfig with the given ID exists, false otherwise. - */ - @Override - public boolean exists(Long id) { - return computeEnvironmentConfigEntityService.exists("id", id); - } - - /** - * Gets a ComputeEnvironmentConfig by its ID. - * - * @param id The ID of the ComputeEnvironmentConfig to retrieve. - * @return The ComputeEnvironmentConfig with the given ID, if it exists or else an empty Optional. - */ - @Override - public Optional retrieve(Long id) { - ComputeEnvironmentConfigEntity entity = computeEnvironmentConfigEntityService.retrieve(id); - return Optional.ofNullable(entity) - .map(ComputeEnvironmentConfigEntity::toPojo); - } - - /** - * Get all ComputeEnvironmentConfigs. - * - * @return A list of all ComputeEnvironmentConfigs. - */ - @Override - public List getAll() { - return computeEnvironmentConfigEntityService - .getAll() - .stream() - .map(ComputeEnvironmentConfigEntity::toPojo) - .collect(Collectors.toList()); - } - - /** - * Creates a new ComputeEnvironmentConfig. - * - * @param type The type of the ComputeEnvironmentConfig to create. - * @return The newly created ComputeEnvironmentConfig. - */ - @Override - public List getByType(ComputeEnvironmentConfig.ConfigType type) { - return computeEnvironmentConfigEntityService - .findByType(type) - .stream() - .map(ComputeEnvironmentConfigEntity::toPojo) - .collect(Collectors.toList()); - } - - /** - * Get all ComputeEnvironmentConfigs that are available to the provided execution scope. A config is available - * provided the execution scope contains all the required access scopes of the config and each scope/id combination - * is enabled. - * - * @param executionScope The execution scope to check availability for. - * Example {Scope.Site: "", Scope.Project: "Project1", Scope.User: "User1"} - * @return A list of ComputeEnvironmentConfigs that are available for the provided execution scope. - */ - @Override - public List getAvailable(Map executionScope) { - return getAvailable(null, executionScope); - } - - /** - * Get the ComputeEnvironmentConfigs of the given type that are available to the provided execution scope. A config - * is available provided the execution scope contains all the required access scopes of the config and each scope/id - * combination is enabled. - * - * @param type The type of the ComputeEnvironmentConfig to get. If null, all types are returned. - * @param executionScope The execution scope to check availability for. - * Example {Scope.Site: "", Scope.Project: "Project1", Scope.User: "User1", Scope.DataType: "xnat:mrSessionData"} - * @return A list of ComputeEnvironmentConfigs that are available for the provided requestedScope. - */ - @Override - public List getAvailable(ComputeEnvironmentConfig.ConfigType type, Map executionScope) { - List all; - - if (type == null) { - all = getAll(); - } else { - all = getByType(type); - } - - final List available = all.stream().filter(computeEnvironmentConfig -> { - Map requiredScopes = computeEnvironmentConfig.getScopes() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - return AccessScope.isEnabledFor(requiredScopes, executionScope); - }).collect(Collectors.toList()); - - available.forEach(computeEnvironmentConfig -> { - // Filter out any hardware configs that are not available to the execution scope - final Set hardwareConfigs = computeEnvironmentConfig.getHardwareOptions().getHardwareConfigs(); - final Set availableHardware = hardwareConfigs.stream() - .filter(hardwareConfig -> { - Map requiredScopes = hardwareConfig.getScopes() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - return AccessScope.isEnabledFor(requiredScopes, executionScope); - }).collect(Collectors.toSet()); - - computeEnvironmentConfig.getHardwareOptions().setHardwareConfigs(availableHardware); - }); - - return available; - } - - /** - * Checks if a ComputeEnvironmentConfig with the given ID is available to the provided scope/id combinations. A - * config is available provided the execution scope contains all the required access scopes of the config and each - * scope/id combination is enabled. - * - * @param id The ID of the ComputeEnvironmentConfig to check availability for. - * @param executionScope The execution scope to check availability for. - * @return True if the ComputeEnvironmentConfig with the given ID is available for the provided execution scope, - * false otherwise. - */ - @Override - public boolean isAvailable(Long id, Map executionScope) { - final Optional computeEnvironmentConfig = retrieve(id); - - if (!computeEnvironmentConfig.isPresent()) { - return false; - } - - Map requiredScopes = computeEnvironmentConfig.get().getScopes() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - return AccessScope.isEnabledFor(requiredScopes, executionScope); - } - - /** - * Creates a new ComputeEnvironmentConfig. - * - * @param computeEnvironmentConfig The ComputeEnvironmentConfig to create. - * @return The newly created ComputeEnvironmentConfig, with its ID set. - */ - @Override - public ComputeEnvironmentConfig create(ComputeEnvironmentConfig computeEnvironmentConfig) { - // Validate the ComputeEnvironmentConfig - validate(computeEnvironmentConfig); - - // Create the new compute environment config entity - ComputeEnvironmentConfigEntity newComputeEnvironmentConfigEntity = computeEnvironmentConfigEntityService.create(ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig)); - - // Update the pojo with the new ID, needed for association with hardware configs - computeEnvironmentConfig.setId(newComputeEnvironmentConfigEntity.getId()); - - setHardwareConfigsForComputeEnvironmentConfig(computeEnvironmentConfig); - - return computeEnvironmentConfigEntityService.retrieve(newComputeEnvironmentConfigEntity.getId()).toPojo(); - } - - /** - * Updates an existing ComputeEnvironmentConfig. - * - * @param computeEnvironmentConfig The ComputeEnvironmentConfig to update. - * @return The updated ComputeEnvironmentConfig. - * @throws NotFoundException If the ComputeEnvironmentConfig to update does not exist. - */ - @Override - public ComputeEnvironmentConfig update(ComputeEnvironmentConfig computeEnvironmentConfig) throws NotFoundException { - // Validate the ComputeEnvironmentConfig - if (computeEnvironmentConfig.getId() == null) { - throw new IllegalArgumentException("ComputeEnvironmentConfig ID cannot be null when updating"); - } - - validate(computeEnvironmentConfig); - - ComputeEnvironmentConfigEntity template = computeEnvironmentConfigEntityService.get(computeEnvironmentConfig.getId()); - template.update(computeEnvironmentConfig); - computeEnvironmentConfigEntityService.update(template); - - // Update the pojo with the new ID, needed for association with hardware configs - computeEnvironmentConfig.setId(template.getId()); - - setHardwareConfigsForComputeEnvironmentConfig(computeEnvironmentConfig); - - return computeEnvironmentConfigEntityService.get(computeEnvironmentConfig.getId()).toPojo(); - } - - /** - * Deletes an existing ComputeEnvironmentConfig. - * - * @param id The ID of the ComputeEnvironmentConfig to delete. - * @throws NotFoundException If the ComputeEnvironmentConfig to delete does not exist. - */ - @Override - public void delete(Long id) throws NotFoundException { - if (!exists(id)) { - throw new NotFoundException("No compute environment config found with ID " + id); - } - - // Remove all hardware configs from the compute environment config - // Get the ids of all hardware configs associated with the compute environment config before deleting to avoid - // a ConcurrentModificationException - Set hardwareConfigIds = computeEnvironmentConfigEntityService.retrieve(id) - .getHardwareOptions() - .getHardwareConfigs() - .stream() - .map(HardwareConfigEntity::getId) - .collect(Collectors.toSet()); - - hardwareConfigIds.forEach(hardwareConfigId -> { - computeEnvironmentConfigEntityService.removeHardwareConfigEntity(id, hardwareConfigId); - }); - - computeEnvironmentConfigEntityService.delete(id); - } - - /** - * Connect the hardware config entities to the compute environment config entity. - * - * @param computeEnvironmentConfig The compute environment config to connect the hardware configs to. - */ - protected void setHardwareConfigsForComputeEnvironmentConfig(ComputeEnvironmentConfig computeEnvironmentConfig) { - // Probably a more efficient way to do this, but it works for now - // Remove all hardware configs from the compute environment config - // Get the ids of all hardware configs associated with the compute environment config before deleting to avoid - // a ConcurrentModificationException - Set hardwareConfigIds = computeEnvironmentConfigEntityService.retrieve(computeEnvironmentConfig.getId()) - .getHardwareOptions() - .getHardwareConfigs() - .stream() - .map(HardwareConfigEntity::getId) - .collect(Collectors.toSet()); - - hardwareConfigIds.forEach(hardwareConfigId -> { - computeEnvironmentConfigEntityService.removeHardwareConfigEntity(computeEnvironmentConfig.getId(), hardwareConfigId); - }); - - // Add the hardware configs to the compute environment config - if (computeEnvironmentConfig.getHardwareOptions().isAllowAllHardware()) { - // Add all hardware configs to the compute environment config - hardwareConfigEntityService.getAll().forEach(hardwareConfigEntity -> { - computeEnvironmentConfigEntityService.addHardwareConfigEntity(computeEnvironmentConfig.getId(), hardwareConfigEntity.getId()); - }); - } else { - // Add the specified hardware configs to the compute environment config - computeEnvironmentConfig.getHardwareOptions().getHardwareConfigs().forEach(hardwareConfig -> { - if (hardwareConfig.getId() == null) { - // cant add a hardware config that doesn't exist - return; - } - - HardwareConfigEntity hardwareConfigEntity = hardwareConfigEntityService.retrieve(hardwareConfig.getId()); - - if (hardwareConfigEntity == null) { - // cant add a hardware config that doesn't exist - return; - } - - computeEnvironmentConfigEntityService.addHardwareConfigEntity(computeEnvironmentConfig.getId(), hardwareConfigEntity.getId()); - }); - } - } - - /** - * Validates the given ComputeEnvironmentConfig. Throws an IllegalArgumentException if the ComputeEnvironmentConfig is invalid. - * - * @param config The ComputeEnvironmentConfig to validate. - */ - protected void validate(ComputeEnvironmentConfig config) { - if (config == null) { - throw new IllegalArgumentException("ComputeEnvironmentConfig cannot be null"); - } - - List errors = new ArrayList<>(); - - errors.addAll(validate(config.getConfigTypes())); - errors.addAll(validate(config.getComputeEnvironment())); - errors.addAll(validate(config.getScopes())); - errors.addAll(validate(config.getHardwareOptions())); - - if (!errors.isEmpty()) { - throw new IllegalArgumentException("ComputeEnvironmentConfig is invalid: " + errors); - } - } - - private List validate(final Set configTypes) { - List errors = new ArrayList<>(); - - if (configTypes == null || configTypes.isEmpty()) { - errors.add("ComputeEnvironmentConfig must have at least one config type"); - } - - return errors; - } - - private List validate(final ComputeEnvironment computeEnvironment) { - List errors = new ArrayList<>(); - - if (computeEnvironment == null) { - errors.add("ComputeEnvironment cannot be null"); - return errors; - } - - if (StringUtils.isBlank(computeEnvironment.getName())) { - errors.add("ComputeEnvironment name cannot be blank"); - } - - if (StringUtils.isBlank(computeEnvironment.getImage())) { - errors.add("ComputeEnvironment image cannot be blank"); - } - - return errors; - } - - private List validate(final Map scopes) { - List errors = new ArrayList<>(); - - if (scopes == null || scopes.isEmpty()) { - errors.add("ComputeEnvironmentConfig must have at least one scope"); - return errors; - } - - if (!scopes.containsKey(Scope.Site)) { - errors.add("ComputeEnvironmentConfig must have a site scope"); - } - - if (!scopes.containsKey(Scope.User)) { - errors.add("ComputeEnvironmentConfig must have a user scope"); - } - - if (!scopes.containsKey(Scope.Project)) { - errors.add("ComputeEnvironmentConfig must have a project scope"); - } - - return errors; - } - - private List validate(final ComputeEnvironmentHardwareOptions hardwareOptions) { - List errors = new ArrayList<>(); - - if (hardwareOptions == null) { - errors.add("ComputeEnvironmentHardwareOptions cannot be null"); - return errors; - } - - if (hardwareOptions.getHardwareConfigs() == null) { - errors.add("ComputeEnvironmentHardwareOptions hardware configs cannot be null"); - } - - return errors; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigService.java deleted file mode 100644 index 6afe944..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigService.java +++ /dev/null @@ -1,195 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; -import org.nrg.framework.constants.Scope; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xnat.compute.entities.ConstraintConfigEntity; -import org.nrg.xnat.compute.models.AccessScope; -import org.nrg.xnat.compute.models.Constraint; -import org.nrg.xnat.compute.models.ConstraintConfig; -import org.nrg.xnat.compute.models.ConstraintScope; -import org.nrg.xnat.compute.services.ConstraintConfigEntityService; -import org.nrg.xnat.compute.services.ConstraintConfigService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -@Service -@Slf4j -public class DefaultConstraintConfigService implements ConstraintConfigService { - - private final ConstraintConfigEntityService constraintConfigEntityService; - - @Autowired - public DefaultConstraintConfigService(ConstraintConfigEntityService constraintConfigEntityService) { - this.constraintConfigEntityService = constraintConfigEntityService; - } - - /** - * Returns the constraint config with the given id. - * - * @param id The id of the constraint config to retrieve - * @return The constraint config with the given id - */ - @Override - public Optional retrieve(Long id) { - ConstraintConfigEntity entity = constraintConfigEntityService.retrieve(id); - return Optional.ofNullable(entity) - .map(ConstraintConfigEntity::toPojo); - } - - /** - * Get all constraint configs. - * - * @return List of all constraint configs - */ - @Override - public List getAll() { - return constraintConfigEntityService - .getAll() - .stream() - .map(ConstraintConfigEntity::toPojo) - .collect(Collectors.toList()); - } - - /** - * Returns all the constraint configs that are available to the provided execution scope. - * - * @param executionScope The scope to get constraint configs for - * (e.g. {Scope.Project: "project1", Scope.User: "user1"}) - * @return All constraint configs that are available for the given scope - */ - @Override - public List getAvailable(Map executionScope) { - return constraintConfigEntityService - .getAll() - .stream() - .map(ConstraintConfigEntity::toPojo) - .filter(config -> { - Map requiredScopes = config.getScopes() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - return AccessScope.isEnabledFor(requiredScopes, executionScope); - }) - .collect(Collectors.toList()); - } - - /** - * Creates a new constraint config. - * - * @param config The constraint config to create - * @return The created constraint config - */ - @Override - public ConstraintConfig create(ConstraintConfig config) { - // Validate - validate(config); - - // Create - ConstraintConfigEntity entity = constraintConfigEntityService.create(ConstraintConfigEntity.fromPojo(config)); - return constraintConfigEntityService.retrieve(entity.getId()).toPojo(); - } - - /** - * Updates the given constraint config. - * - * @param config The constraint config to update - * @return The updated constraint config - * @throws NotFoundException If the constraint config does not exist - */ - @Override - public ConstraintConfig update(ConstraintConfig config) throws NotFoundException { - // Validate - if (config.getId() == null) { - throw new IllegalArgumentException("Constraint config id cannot be null when updating"); - } - - validate(config); - - // Update - ConstraintConfigEntity template = constraintConfigEntityService.retrieve(config.getId()); - template.update(config); - constraintConfigEntityService.update(template); - return constraintConfigEntityService.retrieve(template.getId()).toPojo(); - } - - /** - * Deletes the constraint config with the given id. - * - * @param id The id of the constraint config to delete - */ - @Override - public void delete(Long id) { - constraintConfigEntityService.delete(id); - } - - /** - * Validates that the given constraint config is valid. Throws an IllegalArgumentException if it is not. - * - * @param config The constraint config to validate - */ - protected void validate(ConstraintConfig config) { - if (config == null) { - throw new IllegalArgumentException("Constraint config cannot be null"); - } - - List errors = new ArrayList<>(); - - errors.addAll(validate(config.getConstraint())); - errors.addAll(validate(config.getScopes())); - - if (!errors.isEmpty()) { - throw new IllegalArgumentException("Constraint config is invalid: " + String.join(", ", errors)); - } - } - - private List validate(Constraint constraint) { - List errors = new ArrayList<>(); - - if (constraint == null) { - errors.add("Constraint cannot be null"); - return errors; - } - - if (StringUtils.isBlank(constraint.getKey())) { - errors.add("Constraint key cannot be null or blank"); - } - - if (constraint.getValues() == null || constraint.getValues().isEmpty()) { - errors.add("Constraint values cannot be null or empty"); - } - - if (constraint.getOperator() == null) { - errors.add("Constraint operator cannot be null"); - } - - return errors; - } - - private List validate(Map scopes) { - List errors = new ArrayList<>(); - - if (scopes == null || scopes.isEmpty()) { - errors.add("Scopes cannot be null or empty"); - return errors; - } - - if (!scopes.containsKey(Scope.Site)) { - errors.add("Scopes must contain a site scope"); - } - - if (!scopes.containsKey(Scope.Project)) { - errors.add("Scopes must contain a project scope"); - } - - return errors; - } -} diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java deleted file mode 100644 index c2f74e8..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigService.java +++ /dev/null @@ -1,230 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; -import org.nrg.framework.constants.Scope; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; -import org.nrg.xnat.compute.entities.ComputeEnvironmentHardwareOptionsEntity; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.models.AccessScope; -import org.nrg.xnat.compute.models.Hardware; -import org.nrg.xnat.compute.models.HardwareConfig; -import org.nrg.xnat.compute.models.HardwareScope; -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; -import org.nrg.xnat.compute.services.HardwareConfigEntityService; -import org.nrg.xnat.compute.services.HardwareConfigService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -@Service -@Slf4j -public class DefaultHardwareConfigService implements HardwareConfigService { - - private final HardwareConfigEntityService hardwareConfigEntityService; - private final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService; - - @Autowired - public DefaultHardwareConfigService(final HardwareConfigEntityService hardwareConfigEntityService, - final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService) { - this.hardwareConfigEntityService = hardwareConfigEntityService; - this.computeEnvironmentConfigEntityService = computeEnvironmentConfigEntityService; - } - - /** - * Checks if a hardware config with the given id exists. - * - * @param id The id of the hardware config to check for - * @return True if a hardware config with the given id exists, false otherwise - */ - @Override - public boolean exists(Long id) { - return hardwareConfigEntityService.exists("id", id); - } - - /** - * Returns the hardware config with the given id. - * - * @param id The id of the hardware config to retrieve - * @return An optional containing the hardware config with the given id, or empty if no such hardware config exists - */ - @Override - public Optional retrieve(Long id) { - HardwareConfigEntity entity = hardwareConfigEntityService.retrieve(id); - return Optional.ofNullable(entity).map(HardwareConfigEntity::toPojo); - } - - /** - * Returns all hardware configs - * - * @return List of all hardware configs - */ - @Override - public List retrieveAll() { - return hardwareConfigEntityService - .getAll() - .stream() - .map(HardwareConfigEntity::toPojo) - .collect(Collectors.toList()); - } - - /** - * Creates a new hardware config. - * - * @param hardwareConfig The hardware config to create - * @return The newly created hardware config with an id - */ - @Override - public HardwareConfig create(HardwareConfig hardwareConfig) { - // Validate the hardware config - validate(hardwareConfig); - - // Create the new hardware config entity - HardwareConfigEntity hardwareConfigEntity = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig)); - - // Add the hardware config to all compute environment configs that allow all hardware - computeEnvironmentConfigEntityService.getAll().stream() - .map(ComputeEnvironmentConfigEntity::getHardwareOptions) - .filter(ComputeEnvironmentHardwareOptionsEntity::isAllowAllHardware) - .forEach(hardwareOptions -> { - computeEnvironmentConfigEntityService.addHardwareConfigEntity(hardwareOptions.getId(), hardwareConfigEntity.getId()); - }); - - return hardwareConfigEntityService.retrieve(hardwareConfigEntity.getId()).toPojo(); - } - - /** - * Updates an existing hardware config. - * - * @param hardwareConfig The hardware config to update - * @return The updated hardware config - * @throws NotFoundException If no hardware config exists with the given id - */ - @Override - public HardwareConfig update(HardwareConfig hardwareConfig) throws NotFoundException { - // Validate the hardware config - if (hardwareConfig.getId() == null) { - throw new IllegalArgumentException("Hardware config id cannot be null"); - } - - validate(hardwareConfig); - - // Update the hardware config - HardwareConfigEntity template = hardwareConfigEntityService.get(hardwareConfig.getId()); - template.update(hardwareConfig); - hardwareConfigEntityService.update(template); - return hardwareConfigEntityService.get(hardwareConfig.getId()).toPojo(); - } - - /** - * Deletes the hardware config with the given id. - * - * @param id The id of the hardware config to delete - * @throws NotFoundException If no hardware config exists with the given id - */ - @Override - public void delete(Long id) throws NotFoundException { - if (!exists(id)) { - throw new NotFoundException("No hardware config found with id " + id); - } - - // Remove the hardware config from all compute environment configs - // Probably a more efficient way to do this, but this is the easiest way to do it - computeEnvironmentConfigEntityService.getAll().stream() - .map(ComputeEnvironmentConfigEntity::getHardwareOptions) - .forEach(hardwareOptions -> { - computeEnvironmentConfigEntityService.removeHardwareConfigEntity(hardwareOptions.getId(), id); - }); - - hardwareConfigEntityService.delete(id); - } - - /** - * Checks if the hardware config is available to the provided execution scope. - * - * @param id The id of the hardware config to check - * @param executionScope The execution scope to check the hardware config against - * @return True if the hardware config is available to the provided execution scope, false otherwise - **/ - @Override - public boolean isAvailable(Long id, Map executionScope) { - final Optional hardwareConfig = retrieve(id); - - if (!hardwareConfig.isPresent()) { - log.error("No hardware config found with id " + id); - return false; - } - - Map requiredScopes = hardwareConfig.get().getScopes() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - return AccessScope.isEnabledFor(requiredScopes, executionScope); - } - - /** - * Validates the given hardware config. Throws an IllegalArgumentException if the hardware config is invalid. - * - * @param hardwareConfig The hardware config to validate - */ - protected void validate(HardwareConfig hardwareConfig) { - if (hardwareConfig == null) { - throw new IllegalArgumentException("Hardware config cannot be null"); - } - - List errors = new ArrayList<>(); - - errors.addAll(validate(hardwareConfig.getHardware())); - errors.addAll(validate(hardwareConfig.getScopes())); - - if (!errors.isEmpty()) { - throw new IllegalArgumentException("Hardware config is invalid: " + String.join(", ", errors)); - } - } - - private List validate(final Hardware hardware) { - List errors = new ArrayList<>(); - - if (hardware == null) { - errors.add("Hardware cannot be null"); - return errors; - } - - if (StringUtils.isBlank(hardware.getName())) { - errors.add("Hardware name cannot be blank"); - } - - return errors; - } - - private List validate(final Map scopes) { - List errors = new ArrayList<>(); - - if (scopes == null || scopes.isEmpty()) { - errors.add("Hardware scopes cannot be null or empty"); - return errors; - } - - if (!scopes.containsKey(Scope.Site)) { - errors.add("Hardware scopes must contain a site scope"); - } - - if (!scopes.containsKey(Scope.User)) { - errors.add("Hardware scopes must contain a user scope"); - } - - if (!scopes.containsKey(Scope.Project)) { - errors.add("Hardware scopes must contain a project scope"); - } - - return errors; - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java b/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java deleted file mode 100644 index dc919c6..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateService.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import lombok.extern.slf4j.Slf4j; -import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.models.*; -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; -import org.nrg.xnat.compute.services.ConstraintConfigService; -import org.nrg.xnat.compute.services.HardwareConfigService; -import org.nrg.xnat.compute.services.JobTemplateService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Service -@Slf4j -public class DefaultJobTemplateService implements JobTemplateService { - - private final ComputeEnvironmentConfigService computeEnvironmentConfigService; - private final HardwareConfigService hardwareConfigService; - private final ConstraintConfigService constraintConfigService; - - @Autowired - public DefaultJobTemplateService(final ComputeEnvironmentConfigService computeEnvironmentConfigService, - final HardwareConfigService hardwareConfigService, - final ConstraintConfigService constraintConfigService) { - this.computeEnvironmentConfigService = computeEnvironmentConfigService; - this.hardwareConfigService = hardwareConfigService; - this.constraintConfigService = constraintConfigService; - } - - /** - * Returns true if the specified compute environment config and hardware config are available for the provided - * execution scope and the hardware config is allowed by the compute environment config. - * - * @param computeEnvironmentConfigId the compute environment config id - * @param hardwareConfigId the hardware config id - * @return true if the specified compute environment config and hardware config are available for the provided - * execution scope and the hardware config is allowed by the compute environment config - */ - @Override - public boolean isAvailable(Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope) { - if (computeEnvironmentConfigId == null || hardwareConfigId == null || executionScope == null) { - throw new IllegalArgumentException("One or more parameters is null"); - } - - boolean isComputeEnvironmentConfigAvailable = computeEnvironmentConfigService.isAvailable(computeEnvironmentConfigId, executionScope); - boolean isHardwareConfigAvailable = hardwareConfigService.isAvailable(hardwareConfigId, executionScope); - - if (!isComputeEnvironmentConfigAvailable || !isHardwareConfigAvailable) { - return false; - } - - ComputeEnvironmentConfig computeEnvironmentConfig = computeEnvironmentConfigService - .retrieve(computeEnvironmentConfigId) - .orElseThrow(() -> new IllegalArgumentException("ComputeEnvironmentConfig with id " + computeEnvironmentConfigId + " does not exist")); - - if (computeEnvironmentConfig.getHardwareOptions().isAllowAllHardware()) { - return true; - } - - return computeEnvironmentConfig.getHardwareOptions() - .getHardwareConfigs().stream() - .map(HardwareConfig::getId) - .anyMatch(id -> id.equals(hardwareConfigId)); - } - - /** - * Returns a job template complete with compute environment, hardware, and constraints. - * - * @param computeEnvironmentConfigId the compute environment config id - * @param hardwareConfigId the hardware config id - * @param executionScope the execution scope to verify the compute environment config and hardware are - * available - * @return a job template complete with compute environment, hardware, and constraints - */ - @Override - public JobTemplate resolve(Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope) { - if (computeEnvironmentConfigId == null || hardwareConfigId == null || executionScope == null) { - throw new IllegalArgumentException("One or more parameters is null"); - } - - if (!isAvailable(computeEnvironmentConfigId, hardwareConfigId, executionScope)) { - throw new IllegalArgumentException("JobTemplate resolution failed for computeEnvironmentConfigId " + computeEnvironmentConfigId + " and hardwareConfigId " + hardwareConfigId + " and executionScope " + executionScope); - } - - ComputeEnvironmentConfig computeEnvironmentConfig = computeEnvironmentConfigService - .retrieve(computeEnvironmentConfigId) - .orElseThrow(() -> new IllegalArgumentException("ComputeEnvironmentConfig with id " + computeEnvironmentConfigId + " does not exist")); - - HardwareConfig hardwareConfig = hardwareConfigService - .retrieve(hardwareConfigId) - .orElseThrow(() -> new IllegalArgumentException("HardwareConfig with id " + hardwareConfigId + " does not exist")); - - List constraints = constraintConfigService.getAvailable(executionScope).stream() - .map(ConstraintConfig::getConstraint) - .collect(Collectors.toList()); - - JobTemplate jobTemplate = JobTemplate.builder() - .computeEnvironment(computeEnvironmentConfig.getComputeEnvironment()) - .hardware(hardwareConfig.getHardware()) - .constraints(constraints) - .build(); - - return jobTemplate; - } -} diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityService.java deleted file mode 100644 index 27b95ed..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityService.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import lombok.extern.slf4j.Slf4j; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; -import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; -import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; -import org.nrg.xnat.compute.repositories.HardwareConfigDao; -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.transaction.Transactional; -import java.util.List; - -@Service -@Slf4j -public class HibernateComputeEnvironmentConfigEntityService extends AbstractHibernateEntityService implements ComputeEnvironmentConfigEntityService { - - private final HardwareConfigDao hardwareConfigDao; - - // For testing - public HibernateComputeEnvironmentConfigEntityService(final ComputeEnvironmentConfigDao computeEnvironmentConfigDao, - final HardwareConfigDao hardwareConfigDao) { - super(); - this.hardwareConfigDao = hardwareConfigDao; - setDao(computeEnvironmentConfigDao); - } - - @Autowired - public HibernateComputeEnvironmentConfigEntityService(HardwareConfigDao hardwareConfigDao) { - this.hardwareConfigDao = hardwareConfigDao; - } - - /** - * Associates a hardware config with a compute environment config - * @param computeEnvironmentConfigId The compute environment config id - * @param hardwareConfigId The hardware config id to associate with the compute environment config - */ - @Override - @Transactional - public void addHardwareConfigEntity(Long computeEnvironmentConfigId, Long hardwareConfigId) { - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity = getDao().retrieve(computeEnvironmentConfigId); - HardwareConfigEntity hardwareConfigEntity = hardwareConfigDao.retrieve(hardwareConfigId); - computeEnvironmentConfigEntity.getHardwareOptions().addHardwareConfig(hardwareConfigEntity); - getDao().update(computeEnvironmentConfigEntity); - hardwareConfigDao.update(hardwareConfigEntity); - } - - /** - * Removes association between a hardware config and a compute environment config - * @param computeEnvironmentConfigId The compute environment config id - * @param hardwareConfigId The hardware config id to remove from the compute environment config - */ - @Override - @Transactional - public void removeHardwareConfigEntity(Long computeEnvironmentConfigId, Long hardwareConfigId) { - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity = getDao().retrieve(computeEnvironmentConfigId); - HardwareConfigEntity hardwareConfigEntity = hardwareConfigDao.retrieve(hardwareConfigId); - computeEnvironmentConfigEntity.getHardwareOptions().removeHardwareConfig(hardwareConfigEntity); - getDao().update(computeEnvironmentConfigEntity); - hardwareConfigDao.update(hardwareConfigEntity); - } - - /** - * Returns a list of compute environment configs by type - * @param type The type of compute environment configs to return - * @return A list of compute environment configs of the environmentified type - */ - @Override - @Transactional - public List findByType(ComputeEnvironmentConfig.ConfigType type) { - return getDao().findByType(type.name()); - } - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityService.java deleted file mode 100644 index 76dc177..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityService.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import lombok.extern.slf4j.Slf4j; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; -import org.nrg.xnat.compute.entities.ConstraintConfigEntity; -import org.nrg.xnat.compute.repositories.ConstraintConfigDao; -import org.nrg.xnat.compute.services.ConstraintConfigEntityService; -import org.springframework.stereotype.Service; - -@Service -@Slf4j -public class HibernateConstraintConfigEntityService extends AbstractHibernateEntityService implements ConstraintConfigEntityService { - -} diff --git a/src/main/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityService.java b/src/main/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityService.java deleted file mode 100644 index c70a80d..0000000 --- a/src/main/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityService.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import lombok.extern.slf4j.Slf4j; -import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.repositories.HardwareConfigDao; -import org.nrg.xnat.compute.services.HardwareConfigEntityService; -import org.springframework.stereotype.Service; - -@Service -@Slf4j -public class HibernateHardwareConfigEntityService extends AbstractHibernateEntityService implements HardwareConfigEntityService { - -} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java index 62fe5ab..1510220 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java @@ -2,7 +2,6 @@ import lombok.extern.slf4j.Slf4j; import org.nrg.framework.annotations.XnatPlugin; -import org.nrg.xnat.compute.config.ComputeConfig; import org.nrg.xdat.security.helpers.UserHelper; import org.nrg.xdat.security.services.SearchHelperServiceI; import org.nrg.xnatx.plugins.jupyterhub.client.DefaultJupyterHubClient; @@ -20,7 +19,7 @@ @XnatPlugin(value = "jupyterHubPlugin", name = "XNAT JupyterHub Plugin", logConfigurationFile = "jupyterhub-logback.xml", - entityPackages = {"org.nrg.xnatx.plugins.jupyterhub.entities", "org.nrg.xnat.compute.entities"}) + entityPackages = {"org.nrg.xnatx.plugins.jupyterhub.entities"}) @ComponentScan({"org.nrg.xnatx.plugins.jupyterhub.preferences", "org.nrg.xnatx.plugins.jupyterhub.client", "org.nrg.xnatx.plugins.jupyterhub.rest", @@ -32,7 +31,6 @@ "org.nrg.xnatx.plugins.jupyterhub.utils", "org.nrg.xnatx.plugins.jupyterhub.authorization", "org.nrg.xnatx.plugins.jupyterhub.initialize"}) -@Import({ComputeConfig.class}) @Slf4j public class JupyterHubPlugin { diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-environment-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-environment-configs.js deleted file mode 100644 index 869fe56..0000000 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/compute-environment-configs.js +++ /dev/null @@ -1,1022 +0,0 @@ -console.debug("Loading compute-environment-configs.js"); - -var XNAT = getObject(XNAT || {}); -XNAT.compute = getObject(XNAT.compute|| {}); -XNAT.compute.computeEnvironmentConfigs = getObject(XNAT.compute.computeEnvironmentConfigs || {}); - -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(factory); - } else if (typeof exports === 'object') { - module.exports = factory(); - } else { - return factory(); - } -}(function () { - - XNAT.compute.computeEnvironmentConfigs.get = async (id) => { - console.debug("Fetching compute environment config " + id) - const url = XNAT.url.csrfUrl(`/xapi/compute-environment-configs/${id}`); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error(`Error fetching compute environment config ${id}`); - } - } - - XNAT.compute.computeEnvironmentConfigs.getAll = async (type) => { - console.debug("Fetching compute environment configs") - - const url = type ? - XNAT.url.csrfUrl(`/xapi/compute-environment-configs?type=${type}`) : - XNAT.url.csrfUrl(`/xapi/compute-environment-configs`); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error(`Error fetching compute environment configs`); - } - } - - XNAT.compute.computeEnvironmentConfigs.create = async (computeEnvironment) => { - console.debug("Creating compute environment config") - const url = XNAT.url.csrfUrl(`/xapi/compute-environment-configs`); - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(computeEnvironment) - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error(`Error creating compute environment config`); - } - } - - XNAT.compute.computeEnvironmentConfigs.update = async (computeEnvironment) => { - console.debug("Updating compute environment config") - const id = computeEnvironment['id']; - - if (!id) { - throw new Error(`Cannot update compute environment config without an ID`); - } - - const url = XNAT.url.csrfUrl(`/xapi/compute-environment-configs/${id}`); - - const response = await fetch(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(computeEnvironment) - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error(`Error updating compute environment config ${id}`); - } - } - - XNAT.compute.computeEnvironmentConfigs.save = async (computeEnvironment) => { - console.debug("Saving compute environment config") - const id = computeEnvironment['id']; - - if (id) { - return XNAT.compute.computeEnvironmentConfigs.update(computeEnvironment); - } else { - return XNAT.compute.computeEnvironmentConfigs.create(computeEnvironment); - } - } - - XNAT.compute.computeEnvironmentConfigs.delete = async (id) => { - console.debug("Deleting compute environment config " + id) - const url = XNAT.url.csrfUrl(`/xapi/compute-environment-configs/${id}`); - - const response = await fetch(url, { - method: 'DELETE', - }); - - if (!response.ok) { - throw new Error(`Error deleting compute environment config ${id}`); - } - } - - XNAT.compute.computeEnvironmentConfigs.available = async (type, executionScope) => { - console.debug(`Fetching available compute environment configs for type ${type} and execution scope ${executionScope}`) - let url = type ? - XNAT.url.csrfUrl(`/xapi/compute-environment-configs/available?type=${type}`): - XNAT.url.csrfUrl(`/xapi/compute-environment-configs/available?`); - - // add each scope to the url as a query parameter - for (const scope in executionScope) { - if (executionScope.hasOwnProperty(scope)) { - const value = executionScope[scope]; - if (value) { - url += `&${scope}=${value}`; - } - } - } - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error(`Error fetching available compute environment configs`); - } - } - - XNAT.compute.computeEnvironmentConfigs.manager = async (containerId, computeEnvironmentType) => { - console.debug("Initializing compute environment manager") - - let container, - footer, - computeEnvironmentConfigs = [], - hardwareConfigs = [], - users = [], - projects = []; - - computeEnvironmentType = computeEnvironmentType || ''; - - const init = () => { - container = document.getElementById(containerId); - - if (!container) { - throw new Error(`Cannot find container with id ${containerId}`); - } - - clearContainer(); - renderNewButton(); - refreshTable(); - - getUsers().then(u => users = u.filter(u => u !== 'jupyterhub' && u !== 'guest')); - getProjects().then(p => projects = p); - getHardwareConfigs() - } - - const getUsers = async () => { - let response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users`); - - if (response.ok) { - return response.json(); - } else { - throw new Error(`Error fetching users`); - } - } - - const getProjects = async () => { - let response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/data/projects`); - - if (response.ok) { - let projects = await response.json(); - projects = projects.ResultSet?.Result || []; - projects = projects.map(p => p['ID']); - return projects; - } else { - throw new Error(`Error fetching projects`); - } - } - - const getHardwareConfigs = () => { - XNAT.compute.hardwareConfigs.getAll().then(h => hardwareConfigs = h); - } - - const clearContainer = () => { - container.innerHTML = ''; - } - - const renderNewButton = () => { - footer = container.closest('.panel').querySelector('.panel-footer'); - footer.innerHTML = ''; - - let button = spawn('div', [ - spawn('div.pull-right', [ - spawn('button.btn.btn-sm.submit', { html: 'New ' + (computeEnvironmentType === 'JUPYTERHUB' ? 'Jupyter Environment' : 'computeEnvironment'), onclick: () => editor(null, 'new') }) - ]), - spawn('div.clear.clearFix') - ]) - - footer.appendChild(button); - } - - const enabledToggle = (config) => { - let enabled = config['scopes']['Site']['enabled']; - let ckbox = spawn('input', { - type: 'checkbox', - checked: enabled, - value: enabled ? 'true' : 'false', - data: {checked: enabled}, - onchange: () => { - let enabled = ckbox.checked; - config['scopes']['Site']['enabled'] = enabled; - - XNAT.compute.computeEnvironmentConfigs.update(config).then(() => { - XNAT.ui.banner.top(2000, `Compute Environment ${enabled ? 'Enabled' : 'Disabled'}`, 'success'); - }).catch((err) => { - console.error(err); - XNAT.ui.banner.top(4000, `Error ${enabled ? 'Enabling' : 'Disabling'} Compute Environment`, 'error'); - toggleCheckbox(!enabled); - }); - } - }); - - let toggleCheckbox = (enabled) => { - ckbox.checked = enabled; - ckbox.value = enabled ? 'true' : 'false'; - ckbox.dataset.checked = enabled; - } - - return spawn('label.switchbox', [ckbox, ['span.switchbox-outer', [['span.switchbox-inner']]]]); - } - - const displayUsers = (config) => { - let isAllUsersEnabled = config['scopes']['User']['enabled']; - let users = config['scopes']['User']['ids']; - - if (isAllUsersEnabled) { - return 'All Users'; - } else { - if (users.length > 4) { - function showUserModal(){ - XNAT.dialog.message.open({ - title: 'Enabled Users', - content: '
    • ' + users.join('
    • ') + '
    ', - }); - } - - return spawn('span.show-enabled-users',{ - style: { - 'border-bottom': '1px #ccc dashed', - 'cursor': 'pointer' - }, - data: { 'users': users.join(',') }, - title: 'Click to view users', - onclick: showUserModal - }, - `${users.length} Users Enabled` - ); - } else { - if (users.length === 0) { - return 'No Users Enabled'; - } - - return users.sort().join(', '); - } - } - } - - const displayProjects = (config) => { - let isAllProjectsEnabled = config['scopes']['Project']['enabled']; - let projects = config['scopes']['Project']['ids']; - - if (isAllProjectsEnabled) { - return 'All Projects'; - } else { - if (projects.length > 4) { - function showProjectModal(){ - XNAT.dialog.message.open({ - title: 'Enabled Projects', - content: '
    • ' + projects.join('
    • ') + '
    ', - }); - } - - return spawn('span.show-enabled-projects',{ - style: { - 'border-bottom': '1px #ccc dashed', - 'cursor': 'pointer' - }, - data: { 'projects': projects.join(',') }, - title: 'Click to view projects', - onclick: showProjectModal - }, - `${projects.length} Projects Enabled` - ); - } else { - if (projects.length === 0) { - return 'No Projects Enabled'; - } - - return projects.sort().join(', '); - } - } - } - - const displayHardware = (config) => { - let isAllHardwareEnabled = config['hardwareOptions']['allowAllHardware']; - let hardwareConfigs = config['hardwareOptions']['hardwareConfigs']; - let hardwareConfigNames = hardwareConfigs.map((config) => config['hardware']['name']); - - if (isAllHardwareEnabled) { - return 'All Hardware'; - } else { - if (hardwareConfigs.length > 4) { - function showHardwareModal(){ - XNAT.dialog.message.open({ - title: 'Enabled Hardware', - content: '
    • ' + hardwareConfigNames.join('
    • ') + '
    ', - }); - } - - return spawn('span.show-enabled-hardware',{ - style: { - 'border-bottom': '1px #ccc dashed', - 'cursor': 'pointer' - }, - data: { 'hardware': hardwareConfigNames.join(',') }, - title: 'Click to view hardware', - onclick: showHardwareModal - }, - `${hardwareConfigs.length} Hardware Enabled` - ); - } else { - if (hardwareConfigs.length === 0) { - return 'No Hardware Enabled'; - } - - return hardwareConfigNames.join(', '); - } - } - } - - /** - * Open the editor for a compute environment config - * @param config - the compute environment config to edit - * @param action - the action to perform (new, edit, or copy) - */ - const editor = (config, action) => { - console.debug("Opening compute environment config editor") - let isNew = action === 'new', - isCopy = action === 'copy', - isEdit = action === 'edit', - title = isNew || isCopy ? 'New ' : 'Edit ', - isJupyter = config ? config.configTypes.includes('JUPYTERHUB') : false; - - title = title + (isJupyter ? 'Jupyter Environment' : 'computeEnvironment'); - - - XNAT.dialog.open({ - title: title, - content: spawn('div', { id: 'compute-environment-editor' }), - width: 650, - maxBtn: true, - beforeShow: () => { - const formEl = document.getElementById('compute-environment-editor'); - formEl.classList.add('panel'); - - let id = isNew || isCopy ? '' : config.id; - let type = computeEnvironmentType; - let name = isNew || isCopy ? '' : config.computeEnvironment?.name || ''; - let image = isNew ? '' : config.computeEnvironment?.image; - let environmentVariables = isNew ? [] : config.computeEnvironment?.environmentVariables; - let mounts = isNew ? [] : config.computeEnvironment?.mounts; - - let siteEnabled = isNew ? true : config.scopes?.Site?.enabled; - let allUsersEnabled = isNew ? true : config.scopes?.User?.enabled; - let enabledUserIds = isNew ? [] : config.scopes?.User?.ids; - let allProjectsEnabled = isNew ? true : config.scopes?.Project?.enabled; - let enabledProjectIds = isNew ? [] : config.scopes?.Project?.ids; - - let allHardwareEnabled = isNew ? true : config.hardwareOptions?.allowAllHardware; - let enabledHardwareConfigs = isNew ? [] : config.hardwareOptions?.hardwareConfigs; - - let idEl = XNAT.ui.panel.input.text({ - name: 'id', - id: 'id', - label: 'ID *', - description: 'The ID', - value: id, - }); - - idEl.style.display = 'none'; - - let typeEl = XNAT.ui.panel.input.text({ - name: 'type', - id: 'type', - label: 'Type *', - description: 'The type (e.g. JupyterHub or Container Service)', - value: type, - }); - - typeEl.style.display = 'none'; - - let nameEl = XNAT.ui.panel.input.text({ - name: 'name', - id: 'name', - label: 'Name *', - description: 'User-friendly display name', - value: name, - }); - - let imageEl = XNAT.ui.panel.input.text({ - name: 'image', - id: 'image', - label: 'Image *', - description: 'The Docker image to use. This should be the full image name including the tag. ' + - 'The image must be available on the Docker host where the container will be run.', - value: image, - }); - - let environmentVariablesHeaderEl = spawn('div.environment-variables', [ - spawn('p', 'Environment Variables
    Use this section to define additional ' + - 'environment variables for the container.'), - ]); - - let addEnvironmentVariableButtonEl = spawn('button.btn.btn-sm.add-environment-variable', { - html: 'Add Environment Variable', - style: { 'margin-top': '0.75em' }, - onclick: () => { - addEnvironmentVariable(); - } - }); - - let mountsEl = spawn('div.mounts', [ - spawn('p', 'Mounts
    Use this section to define additional ' + - 'bind mounts to mount into the container.'), - ]); - - let addMountButton = spawn('button.btn.btn-sm.add-mount', { - html: 'Add Mount', - style: { 'margin-top': '0.75em' }, - onclick: () => { - addMount(); - } - }); - - let userAndProjectScopesDescription = spawn('div.user-and-project-scopes', [ - spawn('p', 'Projects and Users
    Use this section to define which projects ' + - 'and users will have access to this.'), - ]); - - let siteEnabledEl = XNAT.ui.panel.input.checkbox({ - name: 'siteEnabled', - id: 'siteEnabled', - label: 'Site Enabled', - description: 'Enable this ComputeEnvironment site-wide.', - value: siteEnabled, - }); - - siteEnabledEl.style.display = 'none'; - - let allUsersEnabledEl = XNAT.ui.panel.input.checkbox({ - name: 'allUsersEnabled', - id: 'allUsersEnabled', - label: 'All Users', - description: 'Enable for all users', - value: allUsersEnabled, - }); - - let userOptions = users.map((user) => { - return { - value: user, - label: user, - selected: enabledUserIds.includes(user) - } - }) - - let userSelectEl = XNAT.ui.panel.select.multiple({ - name: 'userIds', - id: 'userIds', - label: 'Users', - description: 'Select the users who can access this', - options: userOptions, - }); - - // Disable the user select if all users are enabled - allUsersEnabledEl.querySelector('input').addEventListener('change', (event) => { - userSelectEl.querySelector('select').disabled = allUsersEnabledEl.querySelector('input').checked; - }); - - userSelectEl.querySelector('select').disabled = allUsersEnabledEl.querySelector('input').checked; - - // The user select is too small by default - userSelectEl.querySelector('select').style.minWidth = '200px'; - userSelectEl.querySelector('select').style.minHeight = '125px'; - - let allProjectsEnabledEl = XNAT.ui.panel.input.checkbox({ - name: 'allProjectsEnabled', - id: 'allProjectsEnabled', - label: 'All Projects', - description: 'Enable this for all projects', - value: allProjectsEnabled, - }); - - let projectOptions = projects.map((project) => { - return { - value: project, - label: project, - selected: enabledProjectIds.includes(project) - } - }); - - let projectSelectEl = XNAT.ui.panel.select.multiple({ - name: 'projectIds', - id: 'projectIds', - label: 'Projects', - description: 'Select the projects which can access this', - options: projectOptions, - }); - - // Disable the project select if all projects are enabled - allProjectsEnabledEl.querySelector('input').addEventListener('change', (event) => { - projectSelectEl.querySelector('select').disabled = allProjectsEnabledEl.querySelector('input').checked; - }); - - projectSelectEl.querySelector('select').disabled = allProjectsEnabledEl.querySelector('input').checked; - - // The project select is too small by default - projectSelectEl.querySelector('select').style.minWidth = '200px'; - projectSelectEl.querySelector('select').style.minHeight = '125px'; - - let hardwareScopesDescription = spawn('div.hardware-scopes', [ - spawn('p', 'Hardware
    Use this section to define which Hardware ' + - 'this can be used with.'), - ]); - - let allHardwareEnabledEl = XNAT.ui.panel.input.checkbox({ - name: 'allHardwareEnabled', - id: 'allHardwareEnabled', - label: 'All Hardware', - description: 'Allow this to be used with any Hardware.', - value: allHardwareEnabled, - }); - - let hardwareConfigsEl = XNAT.ui.panel.select.multiple({ - name: 'hardwareConfigs', - id: 'hardwareConfigs', - label: 'Hardware Configs', - description: 'Select the Hardware that can be used with this', - options: hardwareConfigs.map((hardwareConfig) => { - return { - value: hardwareConfig.id, - label: hardwareConfig.hardware?.name, - selected: enabledHardwareConfigs.map((enabledHardwareConfig) => enabledHardwareConfig.id).includes(hardwareConfig.id), - } - }), - }); - - // Disable the hardware configs select if all hardware is enabled - allHardwareEnabledEl.querySelector('input').addEventListener('change', (event) => { - hardwareConfigsEl.querySelector('select').disabled = allHardwareEnabledEl.querySelector('input').checked; - }); - - hardwareConfigsEl.querySelector('select').disabled = allHardwareEnabledEl.querySelector('input').checked; - - // The hardware configs select is too small by default - hardwareConfigsEl.querySelector('select').style.minWidth = '200px'; - hardwareConfigsEl.querySelector('select').style.minHeight = '100px'; - - formEl.appendChild(spawn('!', [ - idEl, - typeEl, - nameEl, - imageEl, - spawn('hr'), - environmentVariablesHeaderEl, - addEnvironmentVariableButtonEl, - spawn('hr'), - mountsEl, - addMountButton, - spawn('hr'), - hardwareScopesDescription, - allHardwareEnabledEl, - hardwareConfigsEl, - spawn('hr'), - userAndProjectScopesDescription, - siteEnabledEl, - allProjectsEnabledEl, - projectSelectEl, - allUsersEnabledEl, - userSelectEl, - ])); - - // Add the environment variables - environmentVariables.forEach((environmentVariable) => { - addEnvironmentVariable(environmentVariable); - }); - - // Add the mounts - mounts.forEach((mount) => { - addMount(mount); - }); - }, - buttons: [ - { - label: 'Cancel', - isDefault: false, - close: true - }, - { - label: 'Save', - isDefault: true, - close: false, - action: function() { - const formEl = document.getElementById('compute-environment-editor'); - - const idEl = formEl.querySelector('#id'); - const typeEl = formEl.querySelector('#type'); - const nameEl = formEl.querySelector('#name'); - const imageEl = formEl.querySelector('#image'); - const siteEnabledEl = formEl.querySelector('#siteEnabled'); - const allUsersEnabledEl = formEl.querySelector('#allUsersEnabled'); - const userIdsEl = formEl.querySelector('#userIds'); - const allProjectsEnabledEl = formEl.querySelector('#allProjectsEnabled'); - const projectIdsEl = formEl.querySelector('#projectIds'); - const allHardwareEnabledEl = formEl.querySelector('#allHardwareEnabled'); - const hardwareConfigsEl = formEl.querySelector('#hardwareConfigs'); - - const validators = []; - - let validateNameEl = XNAT.validate(nameEl).reset().chain(); - validateNameEl.is('notEmpty').failure('Name is required'); - validators.push(validateNameEl); - - let validateImageEl = XNAT.validate(imageEl).reset().chain(); - validateImageEl.is('notEmpty').failure('Image is required'); - validators.push(validateImageEl); - - let envVarsPresent = document.querySelectorAll('input.key').length > 0; - if (envVarsPresent) { - let validateEnvironmentVariableKeys = XNAT.validate('input.key').chain(); - validateEnvironmentVariableKeys.is('notEmpty').failure('Keys are required') - .is('regex', /^[a-zA-Z_][a-zA-Z0-9_]*$/).failure('Keys must be valid environment variable names'); - validators.push(validateEnvironmentVariableKeys); - } - - let mountsPresent = document.querySelectorAll('.mount-group').length > 0; - if (mountsPresent) { - let validateLocalPaths = XNAT.validate('.mount-group .local-path').chain(); - validateLocalPaths.is('notEmpty').failure('Local paths are required') - .is('uri').failure('Local paths must be a valid URI'); - validators.push(validateLocalPaths); - - let validateContainerPaths = XNAT.validate('.mount-group .container-path').chain(); - validateContainerPaths.is('notEmpty').failure('Container paths are required') - .is('uri').failure('Container paths must be a valid URI'); - validators.push(validateContainerPaths); - } - - // validate fields - let errorMessages = []; - - validators.forEach((validator) => { - if (!validator.check()) { - validator.messages.forEach(message => errorMessages.push(message)); - } - }); - - if (errorMessages.length > 0) { - XNAT.dialog.open({ - title: 'Error', - width: 400, - content: '
    • ' + errorMessages.join('
    • ') + '
    ', - }) - return; - } - - config = { - id: idEl.value, - configTypes: [typeEl.value], - computeEnvironment: { - name: nameEl.value, - image: imageEl.value, - environmentVariables: getEnvironmentVariables(), - mounts: getMounts(), - }, - scopes: { - Site: { - scope: 'Site', - enabled: siteEnabledEl.checked, - ids: [], - }, - Project: { - scope: 'Project', - enabled: allProjectsEnabledEl.checked, - ids: Array.from(projectIdsEl.selectedOptions).map(option => option.value), - }, - User: { - scope: 'User', - enabled: allUsersEnabledEl.checked, - ids: Array.from(userIdsEl.selectedOptions).map(option => option.value), - } - }, - hardwareOptions: { - allowAllHardware: allHardwareEnabledEl.checked, - hardwareConfigs: Array.from(hardwareConfigsEl.selectedOptions).map(option => { - return { - id: option.value, - } - }), - } - } - - XNAT.compute.computeEnvironmentConfigs.save(config) - .then(() => { - refreshTable(); - XNAT.dialog.closeAll(); - XNAT.ui.banner.top(2000, 'Saved', 'success'); - }) - .catch((err) => { - console.error(err); - XNAT.ui.banner.top(2000, 'Error Saving', 'error'); - }); - } - } - ] - }); - } - - const refreshTable = () => { - XNAT.compute.computeEnvironmentConfigs.getAll(computeEnvironmentType) - .then((data) => { - computeEnvironmentConfigs = data; - - if (computeEnvironmentConfigs.length === 0) { - clearContainer() - container.innerHTML = `

    No Jupyter environments found

    ` - return; - } - - table(); - }) - .catch((err) => { - console.error(err); - clearContainer(); - container.innerHTML = `

    Error loading Jupyter environments

    ` - }); - } - - const deleteConfig = (id) => { - XNAT.compute.computeEnvironmentConfigs.delete(id) - .then(() => { - XNAT.ui.banner.top(2000, 'Delete successful', 'success'); - refreshTable(); - }) - .catch((err) => { - XNAT.ui.banner.top(4000, 'Error deleting', 'error'); - console.error(err); - }); - } - - const addEnvironmentVariable = (envVar) => { - const formEl = document.getElementById('compute-environment-editor'); - const environmentVariablesEl = formEl.querySelector('div.environment-variables'); - - const keyEl = spawn('input.form-control.key', { - id: 'key', - placeholder: 'Key', - type: 'text', - value: envVar ? envVar.key : '', - }); - - const equalsEl = spawn('span.input-group-addon', { - style: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - } - }, ['=']); - - const valueEl = spawn('input.form-control.value', { - id: 'value', - placeholder: 'Value', - type: 'text', - value: envVar ? envVar.value : '', - }); - - const removeButtonEl = spawn('button.btn.btn-danger', { - type: 'button', - title: 'Remove', - onclick: () => { - environmentVariableEl.remove(); - } - }, [ - spawn('i.fa.fa-trash'), - ]); - - const environmentVariableEl = spawn('div.form-group', { - style: { - marginBottom: '5px', - } - }, [ - spawn('div.input-group', { - style: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - columnGap: '10px', - } - }, [ - keyEl, - equalsEl, - valueEl, - spawn('span.input-group-btn', [ - removeButtonEl, - ]), - ]), - ]); - - environmentVariablesEl.appendChild(environmentVariableEl); - } - - const getEnvironmentVariables = () => { - const formEl = document.getElementById('compute-environment-editor'); - const environmentVariablesEl = formEl.querySelector('div.environment-variables'); - - let environmentVariables = []; - - Array.from(environmentVariablesEl.children).forEach((environmentVariableEl) => { - const keyEl = environmentVariableEl.querySelector('#key'); - const valueEl = environmentVariableEl.querySelector('#value'); - - if (keyEl === null || valueEl === null) return; - - environmentVariables.push({ - key: keyEl.value, - value: valueEl.value, - }); - }); - - return environmentVariables; - } - - const addMount = (mount) => { - const formEl = document.getElementById('compute-environment-editor'); - const mountsEl = formEl.querySelector('div.mounts'); - - let removeButton = spawn('a.close', { - html: '', - onclick: () => { - mountGroup.remove(); - } - }) - - let localPath = XNAT.ui.panel.input.text({ - name: 'localPath', - label: 'Local Path', - classes: 'required local-path', - description: 'The local path on the host to mount to the container', - value: mount ? mount.localPath : '', - }); - - let containerPath = XNAT.ui.panel.input.text({ - name: 'containerPath', - label: 'Container Path', - classes: 'required container-path', - description: 'The path to mount the local path to in the container', - value: mount ? mount.containerPath : '', - }); - - let readOnly = XNAT.ui.panel.input.checkbox({ - name: 'readOnly', - label: 'Read Only', - classes: 'required read-only', - description: 'Whether or not the mount should be read only', - value: mount ? mount.readOnly : true, - }); - - let mountGroup = spawn('div.mount-group', { - style: { - border: '1px solid #ccc', - padding: '5px', - margin: '5px', - borderRadius: '10px', - } - }, [ - removeButton, - localPath, - containerPath, - readOnly, - ]); - - mountsEl.appendChild(mountGroup); - } - - const getMounts = () => { - const formEl = document.getElementById('compute-environment-editor'); - const mountsEl = formEl.querySelector('div.mounts'); - - let mounts = []; - - Array.from(mountsEl.children).forEach((mountGroup) => { - const localPathEl = mountGroup.querySelector('.local-path'); - const containerPathEl = mountGroup.querySelector('.container-path'); - const readOnlyEl = mountGroup.querySelector('.read-only'); - - if (localPathEl === null || containerPathEl === null || readOnlyEl === null) return; - - mounts.push({ - volumeName: '', - localPath: localPathEl.value, - containerPath: containerPathEl.value, - readOnly: readOnlyEl.checked, - }); - }); - - return mounts; - } - - const table = () => { - const computeEnvironmentTable = XNAT.table.dataTable(computeEnvironmentConfigs, { - header: true, - sortable: 'name', - columns: { - name: { - label: 'Name', - filter: true, - td: { className: 'word-wrapped align-top' }, - apply: function () { - return this['computeEnvironment']['name']; - } - }, - image: { - label: 'Image', - filter: true, - apply: function () { - return this['computeEnvironment']['image']; - }, - }, - hardware: { - label: 'Hardware', - td: { className: 'hardware word-wrapped align-top' }, - filter: true, - apply: function () { - return displayHardware(this); - }, - }, - projects: { - label: 'Project(s)', - filter: true, - td: { className: 'projects word-wrapped align-top' }, - apply: function () { - return displayProjects(this); - }, - }, - users: { - label: 'User(s)', - filter: true, - td: { className: 'users word-wrapped align-top' }, - apply: function () { - return displayUsers(this); - }, - }, - enabled: { - label: 'Enabled', - apply: function () { - return spawn('div.center', [enabledToggle(this)]); - }, - }, - actions: { - label: 'Actions', - th: { style: { width: '150px' } }, - apply: function () { - return spawn('div.center', [ - spawn('button.btn.btn-sm', { onclick: () => editor(this, 'edit') }, ''), - spawn('span', { style: { display: 'inline-block', width: '4px' } }), - spawn('button.btn.btn-sm', { onclick: () => editor(this, 'copy') }, ''), - spawn('span', { style: { display: 'inline-block', width: '4px' } }), - spawn('button.btn.btn-sm', { onclick: () => deleteConfig(this.id) }, '') - ]); - }, - } - }, - }) - - clearContainer(); - computeEnvironmentTable.render(`#${containerId}`); - } - - init(); - - return { - container: container, - computeEnvironmentConfigs: computeEnvironmentConfigs, - refresh: refreshTable - }; - } - -})); diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/constraint-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/constraint-configs.js deleted file mode 100644 index 13f19ad..0000000 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/constraint-configs.js +++ /dev/null @@ -1,610 +0,0 @@ -console.debug('Loading constraint-configs.js') - -var XNAT = getObject(XNAT || {}); -XNAT.compute = getObject(XNAT.compute|| {}); -XNAT.compute.constraintConfigs = getObject(XNAT.compute.constraintConfigs || {}); - -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(factory); - } else if (typeof exports === 'object') { - module.exports = factory(); - } else { - return factory(); - } -}(function () { - - XNAT.compute.constraintConfigs.get = async (id) => { - console.debug(`Fetching constraint config ${id}`); - const url = XNAT.url.restUrl(`/xapi/constraint-configs/${id}`); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error(`Failed to fetch constraint config ${id}`); - } - } - - XNAT.compute.constraintConfigs.getAll = async () => { - console.debug('Fetching all constraint configs'); - const url = XNAT.url.restUrl('/xapi/constraint-configs'); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error('Failed to fetch constraint configs'); - } - } - - XNAT.compute.constraintConfigs.create = async (constraintConfig) => { - console.debug('Creating constraint config'); - const url = XNAT.url.restUrl('/xapi/constraint-configs'); - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(constraintConfig) - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error('Failed to create constraint config'); - } - } - - XNAT.compute.constraintConfigs.update = async (constraintConfig) => { - console.debug(`Updating constraint config ${constraintConfig.id}`); - const url = XNAT.url.restUrl(`/xapi/constraint-configs/${constraintConfig.id}`); - - const response = await fetch(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(constraintConfig) - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error(`Failed to update constraint config ${constraintConfig.id}`); - } - } - - XNAT.compute.constraintConfigs.delete = async (id) => { - console.debug(`Deleting constraint config ${id}`); - const url = XNAT.url.restUrl(`/xapi/constraint-configs/${id}`); - - const response = await fetch(url, { - method: 'DELETE', - }); - - if (!response.ok) { - throw new Error(`Failed to delete constraint config ${id}`); - } - } - - XNAT.compute.constraintConfigs.save = async (constraintConfig) => { - console.debug(`Saving constraint config ${constraintConfig.id}`); - if (constraintConfig.id) { - return XNAT.compute.constraintConfigs.update(constraintConfig); - } else { - return XNAT.compute.constraintConfigs.create(constraintConfig); - } - } - - XNAT.compute.constraintConfigs.manager = async (containerId) => { - console.debug(`Initializing constraint config manager in container ${containerId}`); - - let container, - footer, - constraintConfigs = [], - users = [], - projects = []; - - const init = () => { - container = document.getElementById(containerId); - - if (!container) { - throw new Error(`Cannot find container with id ${containerId}`); - } - - clearContainer(); - renderNewButton(); - loadProjects(); - refreshTable(); - } - - const clearContainer = () => { - container.innerHTML = ''; - } - - const renderNewButton = () => { - footer = container.closest('.panel').querySelector('.panel-footer'); - footer.innerHTML = ''; - - let button = spawn('div', [ - spawn('div.pull-right', [ - spawn('button.btn.btn-sm.submit', { html: 'New Constraint', onclick: () => editor(null, 'new') }) - ]), - spawn('div.clear.clearFix') - ]) - - footer.appendChild(button); - } - - const loadProjects = async () => { - let response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/data/projects`); - - if (response.ok) { - let data = await response.json(); - data = data.ResultSet?.Result || []; - data = data.map(p => p['ID']); - projects = data; - } else { - throw new Error(`Error fetching projects`); - } - } - - const deleteConfig = (id) => { - XNAT.compute.constraintConfigs.delete(id).then(() => { - XNAT.ui.banner.top(2000, 'Constraint deleted', 'success'); - refreshTable(); - }).catch(err => { - XNAT.ui.banner.top(4000, 'Error deleting constraint', 'error'); - console.error(err); - }); - } - - const displayProjects = (config) => { - let isAllProjectsEnabled = config['scopes']['Project']['enabled']; - let projects = config['scopes']['Project']['ids']; - - if (isAllProjectsEnabled) { - return 'All Projects'; - } else { - if (projects.length > 4) { - function showProjectModal() { - XNAT.dialog.message.open({ - title: 'Enabled Projects', - content: '
    • ' + projects.join('
    • ') + '
    ', - }); - } - - return spawn('span.show-enabled-projects', { - style: { - 'border-bottom': '1px #ccc dashed', - 'cursor': 'pointer' - }, - data: { 'projects': projects.join(',') }, - title: 'Click to view projects', - onclick: showProjectModal - }, - `${projects.length} Projects Enabled` - ); - } else { - if (projects.length === 0) { - return 'No Projects Enabled'; - } - - return projects.sort().join(', '); - } - } - } - - const enabledToggle = (config) => { - let enabled = config['scopes']['Site']['enabled']; - let ckbox = spawn('input', { - type: 'checkbox', - checked: enabled, - value: enabled ? 'true' : 'false', - data: { checked: enabled }, - onchange: () => { - let enabled = ckbox.checked; - config['scopes']['Site']['enabled'] = enabled; - - XNAT.compute.constraintConfigs.update(config).then(() => { - XNAT.ui.banner.top(2000, `Constraint ${enabled ? 'Enabled' : 'Disabled'}`, 'success'); - }).catch((err) => { - console.error(err); - XNAT.ui.banner.top(4000, - `Error ${enabled ? 'Enabling' : 'Disabling'} Constraint`, - 'error' - ); - toggleCheckbox(!enabled); - }); - } - }); - - let toggleCheckbox = (enabled) => { - ckbox.checked = enabled; - ckbox.value = enabled ? 'true' : 'false'; - ckbox.dataset.checked = enabled; - } - - return spawn('label.switchbox', [ckbox, ['span.switchbox-outer', [['span.switchbox-inner']]]]); - } - - const editor = (config, mode) => { - console.debug(`Opening constraint config editor in ${mode} mode`); - - let isNew = mode === 'new', - isEdit = mode === 'edit', - isCopy = mode === 'copy'; - - let title = isNew || isCopy ? 'New Constraint' : 'Edit Constraint'; - - XNAT.dialog.open({ - title: title, - content: spawn('div', { id: 'config-editor' }), - width: 650, - maxBtn: true, - beforeShow: () => { - const form = document.getElementById('config-editor'); - form.classList.add('panel'); - - let id = isNew || isCopy ? '' : config.id; - let attribute = isNew ? '' : config.constraint?.key || ''; - let operator = isNew ? 'IN' : config.constraint?.operator || 'IN'; - let values = isNew ? '' : config.constraint?.values.join(',') || ''; - - let siteEnabled = isNew || isCopy ? true : config.scopes?.Site?.enabled || false; - let allUsersEnabled = isNew ? true : config.scopes?.User?.enabled || false; - let enabledUsers = isNew ? [] : config.scopes?.User?.ids || []; - let allProjectsEnabled = isNew ? true : config.scopes?.Project?.enabled || false; - let enabledProjects = isNew ? [] : config.scopes?.Project?.ids || []; - - let idInput = XNAT.ui.panel.input.text({ - name: 'id', - id: 'id', - label: 'ID *', - description: 'The ID of the constraint', - value: id, - }); - - idInput.style.display = 'none'; // hide the ID field - - let constraintHeader = spawn('div.placement-constraints.swarm', [ - spawn('p', 'Constraint
    Use this section to define a placement ' + - 'constraint applicable to all jobs submitted to this cluster. ' + - 'See Docker Swarm documentation on ' + - 'service placement constraints and the ' + - 'docker service create command ' + - 'for more information about allowed constraints.'), - ]); - - let attributeInput = XNAT.ui.panel.input.text({ - name: `constraint-attribute`, - label: 'Node attribute', - classes: 'required swarm-constraint-attribute', - description: 'Attribute you wish to constrain. E.g., node.labels.mylabel or node.role', - value: attribute, - }); - - let operatorInput = XNAT.ui.panel.input.radioGroup({ - name: `constraint-operator`, // random ID to avoid collisions - label: 'Comparator', - classes: 'required swarm-constraint-operator', - items: { 0: { label: 'Equals', value: 'IN' }, 1: { label: 'Does not equal', value: 'NOT_IN' } }, - value: operator, - }) - - let valuesInput = XNAT.ui.panel.input.list({ - name: `constraint-values`, // random ID to avoid collisions - label: 'Possible Values For Constraint', - classes: 'required swarm-constraint-values', - description: 'Comma-separated list of values on which user can constrain the attribute (or a single value if not user-settable). E.g., "worker" or "spot,demand" (do not add quotes). ', - value: values, - }) - - let projectScopesDescription = spawn('div.user-and-project-scopes', [ - spawn('p', 'Projects
    Use this section to limit the scope of a constraint to ' + - "specific projects (and it's subjects and experiments). "), - ]); - - let siteEnabledInput = XNAT.ui.panel.input.checkbox({ - name: 'siteEnabled', - id: 'siteEnabled', - label: 'Site Enabled', - description: 'Enable this hardware for all users and projects', - value: siteEnabled, - }); - - siteEnabledInput.style.display = 'none'; // hide the site enabled field - - let allUsersEnabledInput = XNAT.ui.panel.input.checkbox({ - name: 'allUsersEnabled', - id: 'allUsersEnabled', - label: 'All Users', - description: 'Enable this hardware for all users', - value: allUsersEnabled, - }); - - allUsersEnabledInput.style.display = 'none'; // hide the all users enabled field - - let userOptions = users.map((user) => { - return { - value: user, - label: user, - selected: enabledUsers.includes(user) - } - }); - - let userSelect = XNAT.ui.panel.select.multiple({ - name: 'enabledUsers', - id: 'enabledUsers', - label: 'Users', - description: 'Select the users to enable this hardware for', - options: userOptions, - }); - - userSelect.style.display = 'none'; // hide the user select - - // Disable the user select if all users are enabled - allUsersEnabledInput.querySelector('input').addEventListener('change', (event) => { - userSelect.querySelector('select').disabled = allUsersEnabledInput.querySelector('input').checked; - }); - - userSelect.querySelector('select').disabled = allUsersEnabledInput.querySelector('input').checked; - - // The user select is too small by default - userSelect.querySelector('select').style.minWidth = '200px'; - userSelect.querySelector('select').style.minHeight = '125px'; - - let allProjectsEnabledInput = XNAT.ui.panel.input.checkbox({ - name: 'allProjectsEnabled', - id: 'allProjectsEnabled', - label: 'All Projects', - description: 'Enable this constraint for all projects', - value: allProjectsEnabled, - }); - - let projectOptions = projects.map((project) => { - return { - value: project, - label: project, - selected: enabledProjects.includes(project) - } - }); - - let projectSelect = XNAT.ui.panel.select.multiple({ - name: 'enabledProjects', - id: 'enabledProjects', - label: 'Projects', - description: 'Select the projects to enable this constraint for', - options: projectOptions, - }); - - // Disable the project select if all projects are enabled - allProjectsEnabledInput.querySelector('input').addEventListener('change', (event) => { - projectSelect.querySelector('select').disabled = allProjectsEnabledInput.querySelector('input').checked; - }); - - projectSelect.querySelector('select').disabled = allProjectsEnabledInput.querySelector('input').checked; - - // The project select is too small by default - projectSelect.querySelector('select').style.minWidth = '200px'; - projectSelect.querySelector('select').style.minHeight = '125px'; - - let formFields = [ - idInput, - constraintHeader, - attributeInput, - operatorInput, - valuesInput, - spawn('hr'), - projectScopesDescription, - siteEnabledInput, - allUsersEnabledInput, - userSelect, - allProjectsEnabledInput, - projectSelect, - ]; - - form.appendChild(spawn('!', formFields)); - }, - buttons: [ - { - label: 'Save', - isDefault: true, - close: false, - action: function () { - const form = document.getElementById('config-editor'); - - let id = form.querySelector('#id'); - let attribute = form.querySelector('.swarm-constraint-attribute'); - let operator = form.querySelector('.swarm-constraint-operator input:checked'); - let values = form.querySelector('.swarm-constraint-values'); - - let siteEnabledElement = form.querySelector('#siteEnabled'); - let allUsersEnabledElement = form.querySelector('#allUsersEnabled'); - let enabledUsersElement = form.querySelector('#enabledUsers'); - let allProjectsEnabledElement = form.querySelector('#allProjectsEnabled'); - let enabledProjectsElement = form.querySelector('#enabledProjects'); - - let validators = [] - - let validateAttribute = XNAT.validate(attribute) - .reset().chain() - .is('notEmpty').failure('Attribute is required'); - validators.push(validateAttribute); - - let validateValues = XNAT.validate(values) - .reset().chain() - .is('notEmpty').failure('Value is required'); - validators.push(validateValues); - - let errors = []; - - validators.forEach((validator) => { - if (!validator.check()) { - validator.messages.forEach(message => errors.push(message)); - } - }); - - if (errors.length > 0) { - XNAT.dialog.open({ - title: 'Error', - width: 400, - content: '
    • ' + errors.join('
    • ') + '
    ', - }) - return; - } - - config = { - id: id.value, - constraint: { - key: attribute.value, - operator: operator.value, - values: values.value.split(',').map((value) => value.trim()), - }, - scopes: { - Site: { - scope: 'Site', - enabled: siteEnabledElement.checked, - }, - Project: { - scope: 'Project', - enabled: allProjectsEnabledElement.checked, - ids: Array.from(enabledProjectsElement.selectedOptions).map(option => option.value), - }, - User: { - scope: 'User', - enabled: allUsersEnabledElement.checked, - ids: Array.from(enabledUsersElement.selectedOptions).map(option => option.value), - }, - } - } - - XNAT.compute.constraintConfigs.save(config).then(() => { - XNAT.ui.banner.top(2000, 'Saved constraint config', 'success'); - XNAT.dialog.closeAll(); - refreshTable(); - }).catch(err => { - console.error(err); - XNAT.ui.banner.top(4000, 'Error saving constraint config', 'error'); - }); - } - }, - { - label: 'Cancel', - close: true, - isDefault: false - } - ] - }); - } - - const refreshTable = () => { - XNAT.compute.constraintConfigs.getAll().then(data => { - constraintConfigs = data; - - if (constraintConfigs.length === 0) { - clearContainer(); - container.innerHTML = `

    No constraints found

    `; - return; - } - - renderTable(); - }).catch(err => { - console.error(err); - clearContainer(); - container.innerHTML = `

    Error fetching constraints

    `; - }) - } - - const renderTable = () => { - let table = XNAT.table.dataTable(constraintConfigs, { - header: true, - sortable: 'nodeAttribute, comparator, values', - columns: { - nodeAttribute: { - label: 'Node Attribute', - td: { className: 'word-wrapped align-top' }, - apply: function() { - return this.constraint?.key; - } - }, - comparator: { - label: 'Comparator', - td: { className: 'word-wrapped align-top' }, - apply: function() { - return spawn('div.center', [this.constraint?.operator === 'IN' ? '==' : '!=']); - } - }, - values: { - label: 'Constraint Value(s)', - td: { className: 'word-wrapped align-top' }, - apply: function() { - return spawn('div.center', [this.constraint?.values?.join(', ')]); - } - }, - projects: { - label: 'Project(s)', - td: { className: 'projects word-wrapped align-top' }, - apply: function () { - return displayProjects(this); - } - }, - enabled: { - label: 'Enabled', - apply: function () { - return spawn('div.center', [enabledToggle(this)]); - }, - }, - actions: { - label: 'Actions', - th: { style: { width: '150px' } }, - apply: function () { - return spawn('div.center', [ - spawn('button.btn.btn-sm', - { onclick: () => editor(this, 'edit') }, - '' - ), - spawn('span', { style: { display: 'inline-block', width: '4px' } }), - spawn('button.btn.btn-sm', - { onclick: () => editor(this, 'copy') }, - '' - ), - spawn('span', { style: { display: 'inline-block', width: '4px' } }), - spawn('button.btn.btn-sm', - { onclick: () => deleteConfig(this.id) }, - '' - ) - ]); - }, - } - } - }); - - clearContainer(); - table.render(`#${containerId}`); - } - - init(); - - return { - container: container, - constraintConfigs: constraintConfigs, - refreshTable: refreshTable - } - } - -})); \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/hardware-configs.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/hardware-configs.js deleted file mode 100644 index 6bbf31f..0000000 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/compute/hardware-configs.js +++ /dev/null @@ -1,1085 +0,0 @@ -console.debug("Loading hardware-configs.js"); - -var XNAT = getObject(XNAT || {}); -XNAT.compute = getObject(XNAT.compute || {}); -XNAT.compute.hardwareConfigs = getObject(XNAT.compute.hardwareConfigs || {}); - -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(factory); - } else if (typeof exports === 'object') { - module.exports = factory(); - } else { - return factory(); - } -}(function () { - - XNAT.compute.hardwareConfigs.get = async (id) => { - console.debug("Fetching hardware config with id: " + id); - const url = XNAT.url.restUrl('/xapi/hardware-configs/' + id); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error('Error fetching hardware config with id: ' + id); - } - } - - XNAT.compute.hardwareConfigs.getAll = async () => { - console.debug("Fetching all hardware configs"); - const url = XNAT.url.restUrl('/xapi/hardware-configs'); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error('Error fetching all hardware configs'); - } - } - - XNAT.compute.hardwareConfigs.create = async (config) => { - console.debug("Creating hardware config"); - const url = XNAT.url.restUrl('/xapi/hardware-configs'); - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(config) - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error('Error creating hardware config'); - } - }; - - XNAT.compute.hardwareConfigs.update = async (config) => { - console.debug("Updating hardware config"); - const id = config.id; - - if (!id) { - throw new Error('Hardware config id is required'); - } - - const url = XNAT.url.restUrl(`/xapi/hardware-configs/${id}`); - - const response = await fetch(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(config) - }); - - if (response.ok) { - return response.json(); - } else { - throw new Error('Error updating hardware config'); - } - } - - XNAT.compute.hardwareConfigs.save = async (config) => { - console.debug("Saving hardware config"); - const id = config.id; - - if (!id) { - return XNAT.compute.hardwareConfigs.create(config); - } else { - return XNAT.compute.hardwareConfigs.update(config); - } - } - - XNAT.compute.hardwareConfigs.delete = async (id) => { - console.debug("Deleting hardware config with id: " + id); - const url = XNAT.url.restUrl('/xapi/hardware-configs/' + id); - - const response = await fetch(url, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (!response.ok) { - throw new Error('Error deleting hardware config with id: ' + id); - } - } - - XNAT.compute.hardwareConfigs.manager = async (containerId) => { - console.debug("Initializing hardware config manager"); - - let container, - footer, - hardwareConfigs = [], - users = [], - projects = []; - - const init = () => { - container = document.getElementById(containerId); - - if (!container) { - throw new Error('Container element wtih id: ' + containerId + ' not found'); - } - - clearContainer(); - renderNewButton(); - - loadUsers(); - loadProjects(); - - refreshTable(); - } - - const loadUsers = async () => { - let response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/xapi/users`); - - if (response.ok) { - let data = await response.json(); - users = data.filter(u => u !== 'jupyterhub' && u !== 'guest'); - } else { - throw new Error(`Error fetching users`); - } - } - - const loadProjects = async () => { - let response = await XNAT.plugin.jupyterhub.utils.fetchWithTimeout(`/data/projects`); - - if (response.ok) { - let data = await response.json(); - data = data.ResultSet?.Result || []; - data = data.map(p => p['ID']); - projects = data; - } else { - throw new Error(`Error fetching projects`); - } - } - - const clearContainer = () => { - container.innerHTML = ''; - } - - const renderNewButton = () => { - footer = container.closest('.panel').querySelector('.panel-footer'); - footer.innerHTML = ''; - - let button = spawn('div', [ - spawn('div.pull-right', [ - spawn('button.btn.btn-sm.submit', { html: 'New Hardware', onclick: () => editor(null, 'new') }) - ]), - spawn('div.clear.clearFix') - ]) - - footer.appendChild(button); - } - - const enabledToggle = (config) => { - let enabled = config['scopes']['Site']['enabled']; - let ckbox = spawn('input', { - type: 'checkbox', - checked: enabled, - value: enabled ? 'true' : 'false', - data: { checked: enabled }, - onchange: () => { - let enabled = ckbox.checked; - config['scopes']['Site']['enabled'] = enabled; - - XNAT.compute.hardwareConfigs.update(config).then(() => { - XNAT.ui.banner.top(2000, `Hardware ${enabled ? 'Enabled' : 'Disabled'}`, 'success'); - }).catch((err) => { - console.error(err); - XNAT.ui.banner.top(4000, - `Error ${enabled ? 'Enabling' : 'Disabling'} Hardware`, - 'error' - ); - toggleCheckbox(!enabled); - }); - } - }); - - let toggleCheckbox = (enabled) => { - ckbox.checked = enabled; - ckbox.value = enabled ? 'true' : 'false'; - ckbox.dataset.checked = enabled; - } - - return spawn('label.switchbox', [ckbox, ['span.switchbox-outer', [['span.switchbox-inner']]]]); - } - - const displayUsers = (config) => { - let isAllUsersEnabled = config['scopes']['User']['enabled']; - let users = config['scopes']['User']['ids']; - - if (isAllUsersEnabled) { - return 'All Users'; - } else { - if (users.length > 4) { - function showUserModal() { - XNAT.dialog.message.open({ - title: 'Enabled Users', - content: '
    • ' + users.join('
    • ') + '
    ', - }); - } - - return spawn('span.show-enabled-users', { - style: { - 'border-bottom': '1px #ccc dashed', - 'cursor': 'pointer' - }, - data: { 'users': users.join(',') }, - title: 'Click to view users', - onclick: showUserModal - }, - `${users.length} Users Enabled` - ); - } else { - if (users.length === 0) { - return 'No Users Enabled'; - } - - return users.sort().join(', '); - } - } - } - - const displayProjects = (config) => { - let isAllProjectsEnabled = config['scopes']['Project']['enabled']; - let projects = config['scopes']['Project']['ids']; - - if (isAllProjectsEnabled) { - return 'All Projects'; - } else { - if (projects.length > 4) { - function showProjectModal() { - XNAT.dialog.message.open({ - title: 'Enabled Projects', - content: '
    • ' + projects.join('
    • ') + '
    ', - }); - } - - return spawn('span.show-enabled-projects', { - style: { - 'border-bottom': '1px #ccc dashed', - 'cursor': 'pointer' - }, - data: { 'projects': projects.join(',') }, - title: 'Click to view projects', - onclick: showProjectModal - }, - `${projects.length} Projects Enabled` - ); - } else { - if (projects.length === 0) { - return 'No Projects Enabled'; - } - - return projects.sort().join(', '); - } - } - } - - const editor = (config, mode) => { - console.debug(`Opening hardware config editor in ${mode} mode`); - - let isNew = mode === 'new', - isEdit = mode === 'edit', - isCopy = mode === 'copy'; - - let title = isNew || isCopy ? 'New Hardware' : 'Edit Hardware'; - - XNAT.dialog.open({ - title: title, - content: spawn('div', { id: 'config-editor' }), - width: 650, - maxBtn: true, - beforeShow: () => { - const form = document.getElementById('config-editor'); - form.classList.add('panel'); - - let id = isNew || isCopy ? '' : config.id; - let name = isNew || isCopy ? '' : config.hardware?.name; - - let cpuLimit = isNew ? '' : config.hardware?.cpuLimit || ''; - let cpuReservation = isNew ? '' : config.hardware?.cpuReservation || ''; - let memoryLimit = isNew ? '' : config.hardware?.memoryLimit || ''; - let memoryReservation = isNew ? '' : config.hardware?.memoryReservation || ''; - - let environmentVariables = isNew ? [] : config.hardware?.environmentVariables || []; - let constraints = isNew ? [] : config.hardware?.constraints || []; - let genericResources = isNew ? [] : config.hardware?.genericResources || []; - - let siteEnabled = isNew || isCopy ? true : config.scopes?.Site?.enabled || false; - let allUsersEnabled = isNew ? true : config.scopes?.User?.enabled || false; - let enabledUsers = isNew ? [] : config.scopes?.User?.ids || []; - let allProjectsEnabled = isNew ? true : config.scopes?.Project?.enabled || false; - let enabledProjects = isNew ? [] : config.scopes?.Project?.ids || []; - - let idInput = XNAT.ui.panel.input.text({ - name: 'id', - id: 'id', - label: 'ID *', - description: 'The ID of the hardware configuration', - value: id, - }); - - idInput.style.display = 'none'; // hide the ID field - - let nameInput = XNAT.ui.panel.input.text({ - name: 'name', - id: 'name', - label: 'Name *', - description: 'Provide a user-friendly name for this hardware', - value: name, - }); - - let cpuAndMemDescriptionSwarm = spawn('div.cpu-mem-header.swarm', [ - spawn('p', 'CPU and Memory
    By default, a container has no resource ' + - 'constraints and can use as much of a given resource as the host’s kernel scheduler allows.' + - ' Docker provides ways to control how much memory, or CPU a container can use, setting ' + - 'runtime configuration flags of the docker run command. ' + - 'For more information, see the Docker documentation on ' + - '' + - 'resource constraints.'), - ]); - - let cpuReservationInput = XNAT.ui.panel.input.text({ - name: 'cpuReservation', - id: 'cpuReservation', - label: 'CPU Reservation', - description: 'The number of CPUs to reserve', - value: cpuReservation, - }); - - let cpuLimitInput = XNAT.ui.panel.input.text({ - name: 'cpuLimit', - id: 'cpuLimit', - label: 'CPU Limit', - description: 'The maximum number of CPUs that can be used', - value: cpuLimit, - }); - - let memoryReservationInput = XNAT.ui.panel.input.text({ - name: 'memoryReservation', - id: 'memoryReservation', - label: 'Memory Reservation', - description: 'The amount of memory that this container will reserve. Allows for suffixes like "0.5G" or "512M".', - value: memoryReservation, - }); - - let memoryLimitInput = XNAT.ui.panel.input.text({ - name: 'memoryLimit', - id: 'memoryLimit', - label: 'Memory Limit', - description: 'The maximum amount of memory that this container can use. Allows for suffixes like "8G" or "4G".', - value: memoryLimit, - }); - - let environmentVariablesHeaderEl = spawn('div.environment-variables', [ - spawn('p', 'Environment Variables
    Use this section to define additional ' + - 'environment variables for the container.'), - ]); - - let addEnvironmentVariableButtonEl = spawn('button.btn.btn-sm.add-environment-variable', { - html: 'Add Environment Variable', - style: { 'margin-top': '0.75em' }, - onclick: () => { - addEnvironmentVariable(); - } - }); - - let genericResourcesDescription = spawn('div.generic-resources.swarm', [ - spawn('p', 'Generic Resources
    Use this section to request generic ' + - 'resources when the container is scheduled. ' + - 'See Docker Swarm documentation on ' + - 'generic resources ' + - 'for more information about allowed constraints.'), - ]); - - let addGenericResourceButton = spawn('button.btn.btn-sm.add-generic-resource.swarm', { - html: 'Add Generic Resource', - style: { 'margin-top': '0.75em' }, - onclick: () => { - addGenericResource(); - } - }); - - let placementConstraintHeaderEl = spawn('div.placement-constraints.swarm', [ - spawn('p', 'Placement Constraints
    Use this section to define placement ' + - 'constraints when the container is scheduled. ' + - 'See Docker Swarm documentation on ' + - 'service placement constraints and the ' + - 'docker service create command ' + - 'for more information about allowed constraints.'), - ]); - - let addSwarmConstraintButtonEl = spawn('button.btn.btn-sm.add-placement-constraint.swarm', { - html: 'Add Constraint', - style: { 'margin-top': '0.75em' }, - onclick: () => { - addSwarmConstraint(); - } - }); - - let userAndProjectScopesDescription = spawn('div.user-and-project-scopes', [ - spawn('p', 'Projects and Users
    Use this section to define which projects ' + - 'and users will have access to this hardware.'), - ]); - - let siteEnabledInput = XNAT.ui.panel.input.checkbox({ - name: 'siteEnabled', - id: 'siteEnabled', - label: 'Site Enabled', - description: 'Enable this hardware for all users and projects', - value: siteEnabled, - }); - - siteEnabledInput.style.display = 'none'; // hide the site enabled field - - let allUsersEnabledInput = XNAT.ui.panel.input.checkbox({ - name: 'allUsersEnabled', - id: 'allUsersEnabled', - label: 'All Users', - description: 'Enable this hardware for all users', - value: allUsersEnabled, - }); - - let userOptions = users.map((user) => { - return { - value: user, - label: user, - selected: enabledUsers.includes(user) - } - }); - - let userSelect = XNAT.ui.panel.select.multiple({ - name: 'enabledUsers', - id: 'enabledUsers', - label: 'Users', - description: 'Select the users to enable this hardware for', - options: userOptions, - }); - - // Disable the user select if all users are enabled - allUsersEnabledInput.querySelector('input').addEventListener('change', (event) => { - userSelect.querySelector('select').disabled = allUsersEnabledInput.querySelector('input').checked; - }); - - userSelect.querySelector('select').disabled = allUsersEnabledInput.querySelector('input').checked; - - // The user select is too small by default - userSelect.querySelector('select').style.minWidth = '200px'; - userSelect.querySelector('select').style.minHeight = '125px'; - - let allProjectsEnabledInput = XNAT.ui.panel.input.checkbox({ - name: 'allProjectsEnabled', - id: 'allProjectsEnabled', - label: 'All Projects', - description: 'Enable this hardware for all projects', - value: allProjectsEnabled, - }); - - let projectOptions = projects.map((project) => { - return { - value: project, - label: project, - selected: enabledProjects.includes(project) - } - }); - - let projectSelect = XNAT.ui.panel.select.multiple({ - name: 'enabledProjects', - id: 'enabledProjects', - label: 'Projects', - description: 'Select the projects to enable this hardware for', - options: projectOptions, - }); - - // Disable the project select if all projects are enabled - allProjectsEnabledInput.querySelector('input').addEventListener('change', (event) => { - projectSelect.querySelector('select').disabled = allProjectsEnabledInput.querySelector('input').checked; - }); - - projectSelect.querySelector('select').disabled = allProjectsEnabledInput.querySelector('input').checked; - - // The project select is too small by default - projectSelect.querySelector('select').style.minWidth = '200px'; - projectSelect.querySelector('select').style.minHeight = '125px'; - - let formFields = [ - idInput, - nameInput, - spawn('hr'), - cpuAndMemDescriptionSwarm, - cpuReservationInput, - cpuLimitInput, - memoryReservationInput, - memoryLimitInput, - spawn('hr'), - environmentVariablesHeaderEl, - addEnvironmentVariableButtonEl, - spawn('hr'), - genericResourcesDescription, - addGenericResourceButton, - spawn('hr'), - placementConstraintHeaderEl, - addSwarmConstraintButtonEl, - spawn('hr'), - userAndProjectScopesDescription, - siteEnabledInput, - allProjectsEnabledInput, - projectSelect, - allUsersEnabledInput, - userSelect, - ]; - - form.appendChild(spawn('!', [ - ...formFields - ])); - - // add the environment variables - environmentVariables.forEach((environmentVariable) => { - addEnvironmentVariable(environmentVariable); - }); - - // add the placement constraints - constraints.forEach((constraint) => { - addSwarmConstraint(constraint); - }); - - // add the generic resources - genericResources.forEach((genericResource) => { - addGenericResource(genericResource); - }); - }, - buttons: [ - { - label: 'Save', - isDefault: true, - close: false, - action: function () { - const form = document.getElementById('config-editor'); - - const idElement = form.querySelector('#id'); - const nameElement = form.querySelector('#name'); - - const cpuReservationElement = form.querySelector('#cpuReservation'); - const cpuLimitElement = form.querySelector('#cpuLimit'); - const memoryReservationElement = form.querySelector('#memoryReservation'); - const memoryLimitElement = form.querySelector('#memoryLimit'); - - const siteEnabledElement = form.querySelector('#siteEnabled'); - const allUsersEnabledElement = form.querySelector('#allUsersEnabled'); - const enabledUsersElement = form.querySelector('#enabledUsers'); - const allProjectsEnabledElement = form.querySelector('#allProjectsEnabled'); - const enabledProjectsElement = form.querySelector('#enabledProjects'); - - const validators = []; - - let validateNameElement = XNAT.validate(nameElement).reset().chain(); - validateNameElement.is('notEmpty').failure('Name is required'); - validators.push(validateNameElement); - - let validateCpuReservationElement = XNAT.validate(cpuReservationElement).reset().chain(); - validateCpuReservationElement.is('allow-empty') - .is('decimal') - .is('greater-than', 0) - .failure('CPU Reservation must be a positive number or empty'); - validators.push(validateCpuReservationElement); - - let validateCpuLimitElement = XNAT.validate(cpuLimitElement).reset().chain(); - validateCpuLimitElement.is('allow-empty') - .is('decimal') - .is('greater-than', 0) - .failure('CPU Limit must be a positive number or empty'); - validators.push(validateCpuLimitElement); - - let validateMemoryReservationElement = XNAT.validate(memoryReservationElement).reset().chain(); - validateMemoryReservationElement.is('allow-empty') - .is('regex', /^$|^([1-9]+[0-9]*)+[KMGT]$/) // 512M, 2G, etc. - .failure('Memory Reservation must be a number followed by a suffix of K, M, G, or T or be empty'); - validators.push(validateMemoryReservationElement); - - let validateMemoryLimitElement = XNAT.validate(memoryLimitElement).reset().chain(); - validateMemoryLimitElement.is('allow-empty') - .is('regex', /^$|^([1-9]+[0-9]*)+[KMGT]$/) // 512M, 2G, etc. - .failure('Memory Limit must be a number followed by a suffix of K, M, G, or T or be empty'); - validators.push(validateMemoryLimitElement); - - let envVarsPresent = document.querySelectorAll('input.key').length > 0; - if (envVarsPresent) { - let validateEnvironmentVariableKeys = XNAT.validate('input.key').chain(); - validateEnvironmentVariableKeys.is('notEmpty') - .is('regex', /^[a-zA-Z_][a-zA-Z0-9_]*$/) // must start with a letter or underscore, and only contain letters, numbers, and underscores - .failure('Keys are required and must be a valid environment variable name'); - validators.push(validateEnvironmentVariableKeys); - } - - let swarmConstraintsPresent = document.querySelectorAll('div.swarm-constraint-group').length > 0; - if (swarmConstraintsPresent) { - let validateSwarmConstraintAttributes = XNAT.validate('input.swarm-constraint-attribute').chain(); - validateSwarmConstraintAttributes.is('notEmpty') - .is('regex', /^[a-zA-Z_][a-zA-Z0-9_.-]*$/) // must start with a letter or underscore, and only contain letters, numbers, underscores, hyphens, and periods - .failure('Attributes are required and must be a valid swarm constraint attribute'); - validators.push(validateSwarmConstraintAttributes); - - let validateSwarmConstraintValues = XNAT.validate('input.swarm-constraint-values').chain(); - validateSwarmConstraintValues.is('notEmpty') - .failure('Constraint values are required'); - validators.push(validateSwarmConstraintValues); - } - - let genericResourcesPresent = document.querySelectorAll('div.resource-group').length > 0; - if (genericResourcesPresent) { - let validateGenericResourceNames = XNAT.validate('input.resource-name').chain(); - validateGenericResourceNames.is('notEmpty').failure('Resource names are required') - .is('regex', /^[a-zA-Z_][a-zA-Z0-9_.-]*$/).failure('Invalid resource name'); - validators.push(validateGenericResourceNames); - - let validateGenericResourceValues = XNAT.validate('input.resource-value').chain(); - validateGenericResourceValues.is('notEmpty').failure('Resource values are required'); - validators.push(validateGenericResourceValues); - } - - // Validate the form - let errorMessages = []; - - validators.forEach((validator) => { - if (!validator.check()) { - validator.messages.forEach(message => errorMessages.push(message)); - } - }); - - if (errorMessages.length > 0) { - XNAT.dialog.open({ - title: 'Error', - width: 400, - content: '
    • ' + errorMessages.join('
    • ') + '
    ', - }) - return; - } - - config = { - id: idElement.value, - hardware: { - name: nameElement.value, - cpuReservation: cpuReservationElement.value, - cpuLimit: cpuLimitElement.value, - memoryReservation: memoryReservationElement.value, - memoryLimit: memoryLimitElement.value, - environmentVariables: getEnvironmentVariables(), - constraints: getSwarmConstraints(), - genericResources: getGenericResources(), - }, - scopes: { - Site: { - scope: 'Site', - enabled: siteEnabledElement.checked, - }, - Project: { - scope: 'Project', - enabled: allProjectsEnabledElement.checked, - ids: Array.from(enabledProjectsElement.selectedOptions).map(option => option.value), - }, - User: { - scope: 'User', - enabled: allUsersEnabledElement.checked, - ids: Array.from(enabledUsersElement.selectedOptions).map(option => option.value), - }, - } - } - - XNAT.compute.hardwareConfigs.save(config).then(() => { - XNAT.ui.banner.top(2000, 'Hardware saved', 'success'); - XNAT.dialog.closeAll(); - refreshTable(); - }).catch(err => { - XNAT.ui.banner.top(4000, 'Error Saving Hardware', 'error'); - console.error(err); - }); - } - }, - { - label: 'Cancel', - close: true, - isDefault: false - } - ] - }) - - } - - const refreshTable = () => { - XNAT.compute.hardwareConfigs.getAll().then(data => { - hardwareConfigs = data; - - if (hardwareConfigs.length === 0) { - clearContainer(); - container.innerHTML = `

    No Hardware found

    `; - return; - } - - // Sort the hardware configs by name - hardwareConfigs = hardwareConfigs.sort((a, b) => { - if (a.name < b.name) { - return -1; - } - if (a.name > b.name) { - return 1; - } - return 0; - }); - - renderTable(); - }).catch(err => { - console.error(err); - clearContainer(); - container.innerHTML = `

    Error loading Hardware

    `; - }); - } - - const deleteConfig = (id) => { - XNAT.compute.hardwareConfigs.delete(id).then(() => { - XNAT.ui.banner.top(2000, 'Hardware config deleted', 'success'); - refreshTable(); - }).catch(err => { - XNAT.ui.banner.top(4000, 'Error deleting hardware config', 'error'); - console.error(err); - }); - } - - const addEnvironmentVariable = (envVar) => { - const formEl = document.getElementById('config-editor'); - const environmentVariablesEl = formEl.querySelector('div.environment-variables'); - - const keyEl = spawn('input.form-control.key', { - id: 'key', - placeholder: 'Key', - type: 'text', - value: envVar ? envVar.key : '', - }); - - const equalsEl = spawn('span.input-group-addon', { - style: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - } - }, ['=']); - - const valueEl = spawn('input.form-control.value', { - id: 'value', - placeholder: 'Value', - type: 'text', - value: envVar ? envVar.value : '', - }); - - const removeButtonEl = spawn('button.btn.btn-danger', { - type: 'button', - title: 'Remove', - onclick: () => { - environmentVariableEl.remove(); - } - }, [ - spawn('i.fa.fa-trash'), - ]); - - const environmentVariableEl = spawn('div.form-group', { - style: { - marginBottom: '5px', - } - }, [ - spawn('div.input-group', { - style: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - columnGap: '10px', - } - }, [ - keyEl, - equalsEl, - valueEl, - spawn('span.input-group-btn', [ - removeButtonEl, - ]), - ]), - ]); - - environmentVariablesEl.appendChild(environmentVariableEl); - } - - const getEnvironmentVariables = () => { - const formEl = document.getElementById('config-editor'); - const environmentVariablesEl = formEl.querySelector('div.environment-variables'); - - let environmentVariables = []; - - Array.from(environmentVariablesEl.children).forEach((environmentVariableEl) => { - const keyEl = environmentVariableEl.querySelector('#key'); - const valueEl = environmentVariableEl.querySelector('#value'); - - if (keyEl === null || valueEl === null) return; - - environmentVariables.push({ - key: keyEl.value, - value: valueEl.value, - }); - }); - - return environmentVariables; - } - - const addSwarmConstraint = (placementConstraint) => { - const formEl = document.getElementById('config-editor'); - const placementConstraintsEl = formEl.querySelector('div.placement-constraints.swarm'); - - let removeButton = spawn('a.close', { - html: '', - onclick: () => { - placementConstraintGroup.remove(); - } - }); - - let attributeInput = XNAT.ui.panel.input.text({ - name: `swarm-constraint-attribute-${Math.floor(Math.random() * 1000000)}`, // random ID to avoid collisions - label: 'Node attribute', - classes: 'required swarm-constraint-attribute', - description: 'Attribute you wish to constrain. E.g., node.labels.mylabel or node.role', - value: placementConstraint ? placementConstraint.key : '', - }); - - let operatorInput = XNAT.ui.panel.input.radioGroup({ - name: `constraint-operator-${Math.floor(Math.random() * 1000000)}`, // random ID to avoid collisions - label: 'Comparator', - classes: 'required swarm-constraint-operator', - items: { 0: { label: 'Equals', value: 'IN' }, 1: { label: 'Does not equal', value: 'NOT_IN' } }, - value: placementConstraint ? placementConstraint.operator : 'IN', - }) - - let valuesInput = XNAT.ui.panel.input.list({ - name: `placement-constraint-values-${Math.floor(Math.random() * 1000000)}`, // random ID to avoid collisions - label: 'Possible Values For Constraint', - classes: 'required swarm-constraint-values', - description: 'Comma-separated list of values on which user can constrain the attribute (or a single value if not user-settable). E.g., "worker" or "spot,demand" (do not add quotes). ', - value: placementConstraint ? placementConstraint.values.join(',') : '', - }) - - let placementConstraintGroup = spawn('div.swarm-constraint-group', { - style: { - border: '1px solid #ccc', - padding: '5px', - margin: '5px', - borderRadius: '10px', - } - }, [ - spawn('div.input-group', [ - removeButton, - attributeInput, - operatorInput, - valuesInput, - ]), - ]); - - placementConstraintsEl.appendChild(placementConstraintGroup); - } - - const getSwarmConstraints = () => { - const formEl = document.getElementById('config-editor'); - const constraintGroups = formEl.querySelectorAll('.swarm-constraint-group'); - - let placementConstraints = []; - - Array.from(constraintGroups).forEach((group) => { - if (group === null) return; - - const attributeEl = group.querySelector('.swarm-constraint-attribute'); - const operatorEl = group.querySelector('.swarm-constraint-operator input:checked'); - const valuesEl = group.querySelector('.swarm-constraint-values'); - - if (attributeEl === null || operatorEl === null || valuesEl === null) return; - - placementConstraints.push({ - key: attributeEl.value, - operator: operatorEl.value, - values: valuesEl.value.split(',').map((value) => value.trim()), - }); - }); - - return placementConstraints; - } - - const addGenericResource = (resource) => { - const formEl = document.getElementById('config-editor'); - const resourcesEl = formEl.querySelector('div.generic-resources'); - - let removeButton = spawn('a.close', { - html: '', - onclick: () => { - resourceGroup.remove(); - } - }); - - let nameInput = XNAT.ui.panel.input.text({ - name: `resource-name-${Math.floor(Math.random() * 1000000)}`, // random ID to avoid collisions - label: 'Name', - classes: 'required resource-name', - description: 'Name of the resource. E.g., GPU', - value: resource ? resource.name : '', - }); - - let valueInput = XNAT.ui.panel.input.text({ - name: `resource-value-${Math.floor(Math.random() * 1000000)}`, // random ID to avoid collisions - label: 'Value', - classes: 'required resource-value', - description: 'Value of the resource. E.g., 1', - value: resource ? resource.value : '', - }); - - let resourceGroup = spawn('div.resource-group', { - style: { - border: '1px solid #ccc', - padding: '5px', - margin: '5px', - borderRadius: '10px', - } - }, [ - spawn('div.input-group', [ - removeButton, - nameInput, - valueInput, - ]), - ]); - - resourcesEl.appendChild(resourceGroup); - } - - const getGenericResources = () => { - const formEl = document.getElementById('config-editor'); - const resourceGroups = formEl.querySelectorAll('.resource-group'); - - let resources = []; - - Array.from(resourceGroups).forEach((group) => { - if (group === null) return; - - const nameEl = group.querySelector('.resource-name'); - const valueEl = group.querySelector('.resource-value'); - - if (nameEl === null || valueEl === null) return; - - resources.push({ - name: nameEl.value, - value: valueEl.value, - }); - }); - - return resources; - } - - const renderTable = () => { - let table = XNAT.table.dataTable(hardwareConfigs, { - header: true, - sortable: 'name', - columns: { - name: { - label: 'Name', - filter: true, - td: { className: 'word-wrapped align-top' }, - apply: function () { - return this.hardware?.name || 'N/A'; - } - }, - cpu: { - label: 'CPU Res / Lim', - filter: true, - apply: function () { - let cpuReservation = this.hardware?.cpuReservation || '-'; - let cpuLimit = this.hardware?.cpuLimit || '-'; - return spawn('div.center', `${cpuReservation} / ${cpuLimit}`); - } - }, - memory: { - label: 'Memory Res / Lim', - filter: true, - apply: function () { - let memoryReservation = this.hardware?.memoryReservation || '-'; - let memoryLimit = this.hardware?.memoryLimit || '-'; - return spawn('div.center', `${memoryReservation} / ${memoryLimit}`); - } - }, - projects: { - label: 'Project(s)', - filter: true, - td: { className: 'projects word-wrapped align-top' }, - apply: function () { - return displayProjects(this); - } - }, - users: { - label: 'User(s)', - filter: true, - td: { className: 'users word-wrapped align-top' }, - apply: function () { - return displayUsers(this); - }, - }, - enabled: { - label: 'Enabled', - apply: function () { - return spawn('div.center', [enabledToggle(this)]); - }, - }, - actions: { - label: 'Actions', - th: { style: { width: '150px' } }, - apply: function () { - return spawn('div.center', [ - spawn('button.btn.btn-sm', - { onclick: () => editor(this, 'edit') }, - '' - ), - spawn('span', { style: { display: 'inline-block', width: '4px' } }), - spawn('button.btn.btn-sm', - { onclick: () => editor(this, 'copy') }, - '' - ), - spawn('span', { style: { display: 'inline-block', width: '4px' } }), - spawn('button.btn.btn-sm', - { onclick: () => deleteConfig(this.id) }, - '' - ) - ]); - }, - } - } - }) - - clearContainer(); - table.render(`#${containerId}`); - } - - init(); - - return { - container: container, - hardwareConfigs: hardwareConfigs, - refreshTable: refreshTable - } - } - -})); \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm index 43defcb..3cd9552 100644 --- a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm +++ b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm @@ -12,5 +12,5 @@ - - \ No newline at end of file + + \ No newline at end of file diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index 82dff63..dd7173a 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -179,9 +179,9 @@ jupyterEnvironments: computeEnvironmentConfigsTable: tag: "div#jupyterhub-compute-environment-configs-table" computeEnvironmentConfigsScript: - tag: script|src="~/scripts/xnat/plugin/compute/compute-environment-configs.js" + tag: script|src="~/scripts/xnat/compute/compute-environment-configs.js" hardwareConfigsScript: - tag: script|src="~/scripts/xnat/plugin/compute/hardware-configs.js" + tag: script|src="~/scripts/xnat/compute/hardware-configs.js" renderComputeEnvironmentConfigsTable: tag: script content: > @@ -203,7 +203,7 @@ hardwareConfigs: hardwareConfigsTable: tag: "div#jupyterhub-hardware-configs-table" hardwareConfigsScript: - tag: script|src="~/scripts/xnat/plugin/compute/hardware-configs.js" + tag: script|src="~/scripts/xnat/compute/hardware-configs.js" renderHardwareConfigsTable: tag: script content: > @@ -225,7 +225,7 @@ constraints: constraintsTable: tag: "div#jupyterhub-constraints-table" constraintsScript: - tag: script|src="~/scripts/xnat/plugin/compute/constraint-configs.js" + tag: script|src="~/scripts/xnat/compute/constraint-configs.js" renderConstraintsTable: tag: script content: > diff --git a/src/test/java/org/nrg/xnat/compute/config/ComputeEnvironmentConfigsApiConfig.java b/src/test/java/org/nrg/xnat/compute/config/ComputeEnvironmentConfigsApiConfig.java deleted file mode 100644 index bd20e1c..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/ComputeEnvironmentConfigsApiConfig.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.nrg.framework.services.ContextService; -import org.nrg.xnat.compute.rest.ComputeEnvironmentConfigsApi; -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.authentication.TestingAuthenticationProvider; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -@Configuration -@EnableWebMvc -@EnableWebSecurity -@Import({MockConfig.class, RestApiTestConfig.class}) -public class ComputeEnvironmentConfigsApiConfig extends WebSecurityConfigurerAdapter { - - @Bean - public ComputeEnvironmentConfigsApi computeEnvironmentConfigsApi(final UserManagementServiceI mockUserManagementService, - final RoleHolder mockRoleHolder, - final ComputeEnvironmentConfigService mockComputeEnvironmentConfigService) { - return new ComputeEnvironmentConfigsApi( - mockUserManagementService, - mockRoleHolder, - mockComputeEnvironmentConfigService - ); - } - - @Bean - public ContextService contextService(final ApplicationContext applicationContext) { - final ContextService contextService = new ContextService(); - contextService.setApplicationContext(applicationContext); - return contextService; - } - - @Override - protected void configure(AuthenticationManagerBuilder auth) { - auth.authenticationProvider(new TestingAuthenticationProvider()); - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/ConstraintConfigsApiTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/ConstraintConfigsApiTestConfig.java deleted file mode 100644 index 5e7fb16..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/ConstraintConfigsApiTestConfig.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.nrg.framework.services.ContextService; -import org.nrg.xnat.compute.rest.ConstraintConfigsApi; -import org.nrg.xnat.compute.services.ConstraintConfigService; -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.authentication.TestingAuthenticationProvider; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -@Configuration -@EnableWebMvc -@EnableWebSecurity -@Import({MockConfig.class, RestApiTestConfig.class}) -public class ConstraintConfigsApiTestConfig extends WebSecurityConfigurerAdapter { - - @Bean - public ConstraintConfigsApi placementConstraintConfigsApi(final UserManagementServiceI mockUserManagementService, - final RoleHolder mockRoleHolder, - final ConstraintConfigService mockConstraintConfigService) { - return new ConstraintConfigsApi( - mockUserManagementService, - mockRoleHolder, - mockConstraintConfigService - ); - } - - @Bean - public ContextService contextService(final ApplicationContext applicationContext) { - final ContextService contextService = new ContextService(); - contextService.setApplicationContext(applicationContext); - return contextService; - } - - @Override - protected void configure(AuthenticationManagerBuilder auth) { - auth.authenticationProvider(new TestingAuthenticationProvider()); - } -} diff --git a/src/test/java/org/nrg/xnat/compute/config/DefaultComputeEnvironmentConfigServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultComputeEnvironmentConfigServiceTestConfig.java deleted file mode 100644 index 5064e54..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/DefaultComputeEnvironmentConfigServiceTestConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; -import org.nrg.xnat.compute.services.HardwareConfigEntityService; -import org.nrg.xnat.compute.services.impl.DefaultComputeEnvironmentConfigService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import({HibernateEntityServicesConfig.class}) -public class DefaultComputeEnvironmentConfigServiceTestConfig { - - @Bean - public DefaultComputeEnvironmentConfigService defaultComputeEnvironmentConfigService(final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService, - final HardwareConfigEntityService hardwareConfigEntityService) { - return new DefaultComputeEnvironmentConfigService( - computeEnvironmentConfigEntityService, - hardwareConfigEntityService - ); - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/DefaultConstraintConfigServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultConstraintConfigServiceTestConfig.java deleted file mode 100644 index d984e94..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/DefaultConstraintConfigServiceTestConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.nrg.xnat.compute.services.ConstraintConfigEntityService; -import org.nrg.xnat.compute.services.impl.DefaultConstraintConfigService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import({HibernateEntityServicesConfig.class}) -public class DefaultConstraintConfigServiceTestConfig { - - @Bean - public DefaultConstraintConfigService defaultConstraintConfigService(final ConstraintConfigEntityService constraintConfigEntityService) { - return new DefaultConstraintConfigService( - constraintConfigEntityService - ); - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/DefaultHardwareConfigServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultHardwareConfigServiceTestConfig.java deleted file mode 100644 index a53ef0d..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/DefaultHardwareConfigServiceTestConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; -import org.nrg.xnat.compute.services.HardwareConfigEntityService; -import org.nrg.xnat.compute.services.impl.DefaultHardwareConfigService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import({HibernateEntityServicesConfig.class}) -public class DefaultHardwareConfigServiceTestConfig { - - @Bean - public DefaultHardwareConfigService defaultHardwareConfigService(final HardwareConfigEntityService hardwareConfigEntityService, - final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService) { - return new DefaultHardwareConfigService( - hardwareConfigEntityService, - computeEnvironmentConfigEntityService - ); - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/DefaultJobTemplateServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/DefaultJobTemplateServiceTestConfig.java deleted file mode 100644 index 190e3be..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/DefaultJobTemplateServiceTestConfig.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; -import org.nrg.xnat.compute.services.ConstraintConfigService; -import org.nrg.xnat.compute.services.HardwareConfigService; -import org.nrg.xnat.compute.services.impl.DefaultJobTemplateService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import({MockConfig.class}) -public class DefaultJobTemplateServiceTestConfig { - - @Bean - public DefaultJobTemplateService defaultJobTemplateService(final ComputeEnvironmentConfigService mockComputeEnvironmentConfigService, - final HardwareConfigService mockHardwareConfigService, - final ConstraintConfigService mockConstraintConfigService) { - return new DefaultJobTemplateService( - mockComputeEnvironmentConfigService, - mockHardwareConfigService, - mockConstraintConfigService - ); - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/HardwareConfigsApiConfig.java b/src/test/java/org/nrg/xnat/compute/config/HardwareConfigsApiConfig.java deleted file mode 100644 index 0571b52..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/HardwareConfigsApiConfig.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.nrg.framework.services.ContextService; -import org.nrg.xnat.compute.rest.HardwareConfigsApi; -import org.nrg.xnat.compute.services.HardwareConfigService; -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.authentication.TestingAuthenticationProvider; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -@Configuration -@EnableWebMvc -@EnableWebSecurity -@Import({MockConfig.class, RestApiTestConfig.class}) -public class HardwareConfigsApiConfig extends WebSecurityConfigurerAdapter { - - @Bean - public HardwareConfigsApi hardwareConfigsApi(final UserManagementServiceI mockUserManagementService, - final RoleHolder mockRoleHolder, - final HardwareConfigService mockHardwareConfigService) { - return new HardwareConfigsApi( - mockUserManagementService, - mockRoleHolder, - mockHardwareConfigService - ); - } - - @Bean - public ContextService contextService(final ApplicationContext applicationContext) { - final ContextService contextService = new ContextService(); - contextService.setApplicationContext(applicationContext); - return contextService; - } - - @Override - protected void configure(AuthenticationManagerBuilder auth) { - auth.authenticationProvider(new TestingAuthenticationProvider()); - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/HibernateComputeEnvironmentConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateComputeEnvironmentConfigEntityServiceTestConfig.java deleted file mode 100644 index d8c46dc..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/HibernateComputeEnvironmentConfigEntityServiceTestConfig.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.hibernate.SessionFactory; -import org.nrg.xnat.compute.entities.*; -import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; -import org.nrg.xnat.compute.repositories.ConstraintConfigDao; -import org.nrg.xnat.compute.repositories.HardwareConfigDao; -import org.nrg.xnat.compute.services.impl.HibernateComputeEnvironmentConfigEntityService; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.orm.hibernate4.HibernateTransactionManager; -import org.springframework.orm.hibernate4.LocalSessionFactoryBean; -import org.springframework.transaction.support.ResourceTransactionManager; - -import javax.sql.DataSource; -import java.util.Properties; - -@Configuration -@Import({HibernateConfig.class}) -public class HibernateComputeEnvironmentConfigEntityServiceTestConfig { - - @Bean - public HibernateComputeEnvironmentConfigEntityService hibernateComputeEnvironmentConfigEntityServiceTest(@Qualifier("computeEnvironmentConfigDaoImpl") final ComputeEnvironmentConfigDao computeEnvironmentConfigDaoImpl, - @Qualifier("hardwareConfigDaoImpl") final HardwareConfigDao hardwareConfigDaoImpl) { - return new HibernateComputeEnvironmentConfigEntityService( - computeEnvironmentConfigDaoImpl, - hardwareConfigDaoImpl); - } - - @Bean - public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qualifier("hibernateProperties") final Properties properties) { - final LocalSessionFactoryBean bean = new LocalSessionFactoryBean(); - bean.setDataSource(dataSource); - bean.setHibernateProperties(properties); - bean.setAnnotatedClasses( - ConstraintConfigEntity.class, - ConstraintEntity.class, - ConstraintScopeEntity.class, - ComputeEnvironmentConfigEntity.class, - ComputeEnvironmentEntity.class, - ComputeEnvironmentScopeEntity.class, - ComputeEnvironmentHardwareOptionsEntity.class, - HardwareConfigEntity.class, - HardwareEntity.class, - HardwareScopeEntity.class, - HardwareConstraintEntity.class, - EnvironmentVariableEntity.class, - MountEntity.class, - GenericResourceEntity.class - ); - return bean; - } - - @Bean - public ResourceTransactionManager transactionManager(final SessionFactory sessionFactory) throws Exception { - return new HibernateTransactionManager(sessionFactory); - } - - @Bean - public ConstraintConfigDao constraintConfigDao(final SessionFactory sessionFactory) { - return new ConstraintConfigDao(sessionFactory); - } - - @Bean - @Qualifier("hardwareConfigDaoImpl") - public HardwareConfigDao hardwareConfigDaoImpl(final SessionFactory sessionFactory) { - return new HardwareConfigDao(sessionFactory); - } - - @Bean - @Qualifier("computeEnvironmentConfigDaoImpl") - public ComputeEnvironmentConfigDao computeEnvironmentConfigDaoImpl(final SessionFactory sessionFactory, - final @Qualifier("hardwareConfigDaoImpl") HardwareConfigDao hardwareConfigDaoImpl) { - return new ComputeEnvironmentConfigDao(sessionFactory, hardwareConfigDaoImpl); - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/HibernateConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateConfig.java deleted file mode 100644 index 472d9af..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/HibernateConfig.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.apache.commons.dbcp2.BasicDataSource; -import org.hibernate.SessionFactory; -import org.nrg.xnat.compute.entities.*; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.config.PropertiesFactoryBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.orm.hibernate4.HibernateTransactionManager; -import org.springframework.orm.hibernate4.LocalSessionFactoryBean; -import org.springframework.transaction.support.ResourceTransactionManager; - -import javax.sql.DataSource; -import java.io.IOException; -import java.util.Properties; - -@Configuration -public class HibernateConfig { - @Bean - public Properties hibernateProperties() throws IOException { - Properties properties = new Properties(); - - // Use HSQLDialect instead of H2Dialect to work around issue - // with h2 version 1.4.200 in hibernate < 5.4 (or so) - // where the generated statements to drop tables between tests can't be executed - // as they do not cascade. - // See https://hibernate.atlassian.net/browse/HHH-13711 - // Solution from https://github.com/hibernate/hibernate-orm/pull/3093#issuecomment-562752874 - properties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); - properties.put("hibernate.hbm2ddl.auto", "create"); - properties.put("hibernate.cache.use_second_level_cache", false); - properties.put("hibernate.cache.use_query_cache", false); - - PropertiesFactoryBean hibernate = new PropertiesFactoryBean(); - hibernate.setProperties(properties); - hibernate.afterPropertiesSet(); - return hibernate.getObject(); - } - - @Bean - public DataSource dataSource() { - BasicDataSource basicDataSource = new BasicDataSource(); - basicDataSource.setDriverClassName(org.h2.Driver.class.getName()); - basicDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); - basicDataSource.setUsername("sa"); - return basicDataSource; - } - - @Bean - public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qualifier("hibernateProperties") final Properties properties) { - final LocalSessionFactoryBean bean = new LocalSessionFactoryBean(); - bean.setDataSource(dataSource); - bean.setHibernateProperties(properties); - bean.setAnnotatedClasses( - ConstraintConfigEntity.class, - ConstraintEntity.class, - ConstraintScopeEntity.class, - ComputeEnvironmentConfigEntity.class, - ComputeEnvironmentEntity.class, - ComputeEnvironmentScopeEntity.class, - ComputeEnvironmentHardwareOptionsEntity.class, - HardwareConfigEntity.class, - HardwareEntity.class, - HardwareScopeEntity.class, - HardwareConstraintEntity.class, - EnvironmentVariableEntity.class, - MountEntity.class, - GenericResourceEntity.class - ); - return bean; - } - - @Bean - public ResourceTransactionManager transactionManager(final SessionFactory sessionFactory) throws Exception { - return new HibernateTransactionManager(sessionFactory); - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/HibernateConstraintConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateConstraintConfigEntityServiceTestConfig.java deleted file mode 100644 index b1f228c..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/HibernateConstraintConfigEntityServiceTestConfig.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.hibernate.SessionFactory; -import org.nrg.xnat.compute.entities.*; -import org.nrg.xnat.compute.repositories.ConstraintConfigDao; -import org.nrg.xnat.compute.services.impl.HibernateConstraintConfigEntityService; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.orm.hibernate4.HibernateTransactionManager; -import org.springframework.orm.hibernate4.LocalSessionFactoryBean; -import org.springframework.transaction.support.ResourceTransactionManager; - -import javax.sql.DataSource; -import java.util.Properties; - -@Configuration -@Import({MockConfig.class, HibernateConfig.class}) -public class HibernateConstraintConfigEntityServiceTestConfig { - - @Bean - public HibernateConstraintConfigEntityService hibernateConstraintConfigEntityServiceTest() { - return new HibernateConstraintConfigEntityService(); - } - - @Bean - public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qualifier("hibernateProperties") final Properties properties) { - final LocalSessionFactoryBean bean = new LocalSessionFactoryBean(); - bean.setDataSource(dataSource); - bean.setHibernateProperties(properties); - bean.setAnnotatedClasses( - ConstraintConfigEntity.class, - ConstraintEntity.class, - ConstraintScopeEntity.class, - ComputeEnvironmentConfigEntity.class, - ComputeEnvironmentEntity.class, - ComputeEnvironmentScopeEntity.class, - ComputeEnvironmentHardwareOptionsEntity.class, - HardwareConfigEntity.class, - HardwareEntity.class, - HardwareScopeEntity.class, - HardwareConstraintEntity.class, - EnvironmentVariableEntity.class, - MountEntity.class, - GenericResourceEntity.class - ); - return bean; - } - - @Bean - public ResourceTransactionManager transactionManager(final SessionFactory sessionFactory) throws Exception { - return new HibernateTransactionManager(sessionFactory); - } - - @Bean - public ConstraintConfigDao constraintConfigDao(final SessionFactory sessionFactory) { - return new ConstraintConfigDao(sessionFactory); - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/HibernateEntityServicesConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateEntityServicesConfig.java deleted file mode 100644 index eb3ef86..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/HibernateEntityServicesConfig.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.hibernate.SessionFactory; -import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; -import org.nrg.xnat.compute.repositories.ConstraintConfigDao; -import org.nrg.xnat.compute.repositories.HardwareConfigDao; -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; -import org.nrg.xnat.compute.services.ConstraintConfigEntityService; -import org.nrg.xnat.compute.services.HardwareConfigEntityService; -import org.nrg.xnat.compute.services.impl.HibernateComputeEnvironmentConfigEntityService; -import org.nrg.xnat.compute.services.impl.HibernateConstraintConfigEntityService; -import org.nrg.xnat.compute.services.impl.HibernateHardwareConfigEntityService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import({HibernateConfig.class}) -public class HibernateEntityServicesConfig { - - @Bean - public ConstraintConfigDao constraintConfigDao(final SessionFactory sessionFactory) { - return new ConstraintConfigDao(sessionFactory); - } - - @Bean - public ConstraintConfigEntityService constraintConfigEntityService(final ConstraintConfigDao constraintConfigDao) { - HibernateConstraintConfigEntityService service = new HibernateConstraintConfigEntityService(); - service.setDao(constraintConfigDao); - return service; - } - - @Bean - public HardwareConfigDao hardwareConfigDao(final SessionFactory sessionFactory) { - return new HardwareConfigDao(sessionFactory); - } - - @Bean - public ComputeEnvironmentConfigDao computeEnvironmentConfigDao(final SessionFactory sessionFactory, - final HardwareConfigDao hardwareConfigDao) { - return new ComputeEnvironmentConfigDao(sessionFactory, hardwareConfigDao); - } - - @Bean - public ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService(final ComputeEnvironmentConfigDao computeEnvironmentConfigDao, - final HardwareConfigDao hardwareConfigDao) { - return new HibernateComputeEnvironmentConfigEntityService( - computeEnvironmentConfigDao, - hardwareConfigDao - ); - } - - @Bean - public HardwareConfigEntityService hardwareConfigEntityService(final HardwareConfigDao hardwareConfigDao) { - HibernateHardwareConfigEntityService service = new HibernateHardwareConfigEntityService(); - service.setDao(hardwareConfigDao); - return service; - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/HibernateHardwareConfigEntityServiceTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/HibernateHardwareConfigEntityServiceTestConfig.java deleted file mode 100644 index 71efe1b..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/HibernateHardwareConfigEntityServiceTestConfig.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.hibernate.SessionFactory; -import org.nrg.xnat.compute.entities.*; -import org.nrg.xnat.compute.repositories.ConstraintConfigDao; -import org.nrg.xnat.compute.services.impl.HibernateHardwareConfigEntityService; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.orm.hibernate4.HibernateTransactionManager; -import org.springframework.orm.hibernate4.LocalSessionFactoryBean; -import org.springframework.transaction.support.ResourceTransactionManager; - -import javax.sql.DataSource; -import java.util.Properties; - -@Configuration -@Import({MockConfig.class, HibernateConfig.class}) -public class HibernateHardwareConfigEntityServiceTestConfig { - - @Bean - public HibernateHardwareConfigEntityService hibernateHardwareConfigEntityServiceTest() { - return new HibernateHardwareConfigEntityService(); - } - - @Bean - public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qualifier("hibernateProperties") final Properties properties) { - final LocalSessionFactoryBean bean = new LocalSessionFactoryBean(); - bean.setDataSource(dataSource); - bean.setHibernateProperties(properties); - bean.setAnnotatedClasses( - ConstraintConfigEntity.class, - ConstraintEntity.class, - ConstraintScopeEntity.class, - ComputeEnvironmentConfigEntity.class, - ComputeEnvironmentEntity.class, - ComputeEnvironmentScopeEntity.class, - ComputeEnvironmentHardwareOptionsEntity.class, - HardwareConfigEntity.class, - HardwareEntity.class, - HardwareScopeEntity.class, - HardwareConstraintEntity.class, - EnvironmentVariableEntity.class, - MountEntity.class, - GenericResourceEntity.class - ); - return bean; - } - - @Bean - public ResourceTransactionManager transactionManager(final SessionFactory sessionFactory) throws Exception { - return new HibernateTransactionManager(sessionFactory); - } - - @Bean - public ConstraintConfigDao constraintConfigDao(final SessionFactory sessionFactory) { - return new ConstraintConfigDao(sessionFactory); - } - -} diff --git a/src/test/java/org/nrg/xnat/compute/config/MockConfig.java b/src/test/java/org/nrg/xnat/compute/config/MockConfig.java deleted file mode 100644 index 11ff2ec..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/MockConfig.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.nrg.xnat.compute.config; - -import org.mockito.Mockito; -import org.nrg.framework.services.SerializerService; -import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; -import org.nrg.xnat.compute.repositories.HardwareConfigDao; -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xdat.security.services.RoleServiceI; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xnat.compute.services.*; -import org.nrg.xnat.services.XnatAppInfo; -import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; - -@Configuration -public class MockConfig { - - @Bean - public NamedParameterJdbcTemplate mockNamedParameterJdbcTemplate() { - return Mockito.mock(NamedParameterJdbcTemplate.class); - } - - @Bean - public UserManagementServiceI mockUserManagementServiceI() { - return Mockito.mock(UserManagementServiceI.class); - } - - @Bean - @Qualifier("mockRoleService") - public RoleServiceI mockRoleService() { - return Mockito.mock(RoleServiceI.class); - } - - @Bean - public RoleHolder mockRoleHolder(@Qualifier("mockRoleService") final RoleServiceI mockRoleService, - final NamedParameterJdbcTemplate mockNamedParameterJdbcTemplate) { - return new RoleHolder(mockRoleService, mockNamedParameterJdbcTemplate); - } - - @Bean - public XFTManagerHelper mockXFTManagerHelper() { - return Mockito.mock(XFTManagerHelper.class); - } - - @Bean - public SerializerService mockSerializerService() { - return Mockito.mock(SerializerService.class); - } - - @Bean - public XnatAppInfo mockXnatAppInfo() { - return Mockito.mock(XnatAppInfo.class); - } - - @Bean - public ConstraintConfigService mockPlacementConstraintConfigService() { - return Mockito.mock(ConstraintConfigService.class); - } - - @Bean - public ComputeEnvironmentConfigService mockComputeEnvironmentConfigService() { - return Mockito.mock(ComputeEnvironmentConfigService.class); - } - - @Bean - @Qualifier("mockComputeEnvironmentConfigEntityService") - public ComputeEnvironmentConfigEntityService mockComputeEnvironmentConfigEntityService() { - return Mockito.mock(ComputeEnvironmentConfigEntityService.class); - } - - @Bean - public HardwareConfigService mockHardwareConfigService() { - return Mockito.mock(HardwareConfigService.class); - } - - @Bean - @Qualifier("mockHardwareConfigEntityService") - public HardwareConfigEntityService mockHardwareConfigEntityService() { - return Mockito.mock(HardwareConfigEntityService.class); - } - - @Bean - @Qualifier("mockHardwareConfigDao") - public HardwareConfigDao mockHardwareConfigDao() { - return Mockito.mock(HardwareConfigDao.class); - } - - @Bean - @Qualifier("mockComputeEnvironmentConfigDao") - public ComputeEnvironmentConfigDao mockComputeEnvironmentConfigDao() { - return Mockito.mock(ComputeEnvironmentConfigDao.class); - } - - @Bean - public JobTemplateService mockJobTemplateService() { - return Mockito.mock(JobTemplateService.class); - } - - @Bean - public ConstraintConfigEntityService mockConstraintConfigEntityService() { - return Mockito.mock(ConstraintConfigEntityService.class); - } -} diff --git a/src/test/java/org/nrg/xnat/compute/config/ObjectMapperConfig.java b/src/test/java/org/nrg/xnat/compute/config/ObjectMapperConfig.java deleted file mode 100644 index eb5b5ad..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/ObjectMapperConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.nrg.xnat.compute.config; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.guava.GuavaModule; -import com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; - -@Configuration -public class ObjectMapperConfig { - @Bean - public Jackson2ObjectMapperBuilder objectMapperBuilder() { - return new Jackson2ObjectMapperBuilder() - .serializationInclusion(JsonInclude.Include.NON_NULL) - .failOnEmptyBeans(false) - .featuresToEnable(JsonParser.Feature.ALLOW_SINGLE_QUOTES, JsonParser.Feature.ALLOW_YAML_COMMENTS) - .featuresToDisable(SerializationFeature.FAIL_ON_EMPTY_BEANS, SerializationFeature.WRITE_NULL_MAP_VALUES) - .modulesToInstall(new Hibernate4Module(), new GuavaModule()); - } - - @Bean - public ObjectMapper objectMapper() { - return objectMapperBuilder().build(); - } -} diff --git a/src/test/java/org/nrg/xnat/compute/config/RestApiTestConfig.java b/src/test/java/org/nrg/xnat/compute/config/RestApiTestConfig.java deleted file mode 100644 index 6016ff6..0000000 --- a/src/test/java/org/nrg/xnat/compute/config/RestApiTestConfig.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.nrg.xnat.compute.config; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.mockito.Mockito; -import org.nrg.mail.services.MailService; -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xdat.security.services.RoleServiceI; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xnat.services.XnatAppInfo; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.StringHttpMessageConverter; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; - -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import static org.mockito.Mockito.when; - -@Configuration -@Import({ObjectMapperConfig.class}) -public class RestApiTestConfig extends WebMvcConfigurerAdapter { - @Bean - @Qualifier("mockXnatAppInfo") - public XnatAppInfo mockAppInfo() { - XnatAppInfo mockXnatAppInfo = Mockito.mock(XnatAppInfo.class); - when(mockXnatAppInfo.isPrimaryNode()).thenReturn(true); - return mockXnatAppInfo; - } - - @Bean - public ExecutorService executorService() { - return Executors.newCachedThreadPool(); - } - - @Bean - public ThreadPoolExecutorFactoryBean syncThreadPoolExecutorFactoryBean(ExecutorService executorService) { - ThreadPoolExecutorFactoryBean tBean = Mockito.mock(ThreadPoolExecutorFactoryBean.class); - when(tBean.getObject()).thenReturn(executorService); - return tBean; - } - - @Bean - public MailService mockMailService() { - return Mockito.mock(MailService.class); - } - - @Bean - @Qualifier("mockRoleService") - public RoleServiceI mockRoleService() { - return Mockito.mock(RoleServiceI.class); - } - - @Bean - public RoleHolder mockRoleHolder(@Qualifier("mockRoleService") final RoleServiceI roleServiceI, - final NamedParameterJdbcTemplate namedParameterJdbcTemplate) { - return new RoleHolder(roleServiceI, namedParameterJdbcTemplate); - } - - @Bean - public NamedParameterJdbcTemplate mockNamedParameterJdbcTemplate() { - return Mockito.mock(NamedParameterJdbcTemplate.class); - } - - @Bean - public UserManagementServiceI mockUserManagementService() { - return Mockito.mock(UserManagementServiceI.class); - } - - @Override - public void configureMessageConverters(List> converters) { - converters.add(new StringHttpMessageConverter()); - converters.add(new MappingJackson2HttpMessageConverter(objectMapper)); - } - - @Autowired - private ObjectMapper objectMapper; -} diff --git a/src/test/java/org/nrg/xnat/compute/models/ConstraintTest.java b/src/test/java/org/nrg/xnat/compute/models/ConstraintTest.java deleted file mode 100644 index cfcb81d..0000000 --- a/src/test/java/org/nrg/xnat/compute/models/ConstraintTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.nrg.xnat.compute.models; - -import org.junit.Test; -import org.nrg.xnat.compute.models.Constraint; - -import java.util.Arrays; -import java.util.HashSet; - -import static org.junit.Assert.*; - -public class ConstraintTest { - - @Test - public void testListConversion_IN() { - Constraint constraint = Constraint.builder() - .key("node.instance.type") - .operator(Constraint.Operator.IN) - .values(new HashSet<>(Arrays.asList("spot", "demand"))) - .build(); - - assertEquals(Arrays.asList("node.instance.type==spot", "node.instance.type==demand"), constraint.toList()); - } - - @Test - public void testListConversion_NOT_IN() { - Constraint constraint = Constraint.builder() - .key("node.instance.type") - .operator(Constraint.Operator.NOT_IN) - .values(new HashSet<>(Arrays.asList("spot", "demand"))) - .build(); - - assertEquals(Arrays.asList("node.instance.type!=spot", "node.instance.type!=demand"), constraint.toList()); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApiTest.java b/src/test/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApiTest.java deleted file mode 100644 index 879a36d..0000000 --- a/src/test/java/org/nrg/xnat/compute/rest/ComputeEnvironmentConfigsApiTest.java +++ /dev/null @@ -1,239 +0,0 @@ -package org.nrg.xnat.compute.rest; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.config.ComputeEnvironmentConfigsApiConfig; -import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; -import org.nrg.xdat.security.services.RoleServiceI; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xft.security.UserI; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.util.NestedServletException; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static org.mockito.Mockito.*; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebAppConfiguration -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {ComputeEnvironmentConfigsApiConfig.class}) -public class ComputeEnvironmentConfigsApiTest { - - @Autowired private WebApplicationContext wac; - @Autowired private ObjectMapper mapper; - @Autowired private RoleServiceI mockRoleService; - @Autowired private UserManagementServiceI mockUserManagementService; - @Autowired private ComputeEnvironmentConfigService mockComputeEnvironmentConfigService; - - private MockMvc mockMvc; - private UserI mockUser; - private Authentication mockAuthentication; - - private ComputeEnvironmentConfig computeEnvironmentConfig; - - @Before - public void before() throws Exception { - mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); - - // Mock the user - mockUser = Mockito.mock(UserI.class); - when(mockUser.getLogin()).thenReturn("mockUser"); - when(mockUser.getEmail()).thenReturn("mockUser@mockuser.com"); - when(mockUser.getPassword()).thenReturn("mockUserPassword"); - when(mockUser.getID()).thenReturn(1); - when(mockRoleService.isSiteAdmin(mockUser)).thenReturn(true); - mockAuthentication = new TestingAuthenticationToken(mockUser, mockUser.getPassword()); - - // Setup the compute environment config - computeEnvironmentConfig = new ComputeEnvironmentConfig(); - computeEnvironmentConfig.setId(1L); - } - - @After - public void after() throws Exception { - Mockito.reset( - mockRoleService, - mockUserManagementService, - mockComputeEnvironmentConfigService, - mockUser - ); - } - - @Test - public void testGetAllComputeEnvironmentConfigs() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/compute-environment-configs") - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify that the service was called - verify(mockComputeEnvironmentConfigService, times(1)).getAll(); - verify(mockComputeEnvironmentConfigService, never()).getByType(any()); - } - - @Test - public void testGetComputeEnvironmentConfigsByType() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/compute-environment-configs") - .param("type", "JUPYTERHUB") - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify that the service was called - verify(mockComputeEnvironmentConfigService, never()).getAll(); - verify(mockComputeEnvironmentConfigService, times(1)).getByType(any()); - } - - @Test - public void testGetComputeEnvironmentConfigById() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/compute-environment-configs/1") - .accept(APPLICATION_JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); - - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify that the service was called - verify(mockComputeEnvironmentConfigService, times(1)).retrieve(any()); - } - - @Test - public void testGetComputeEnvironmentConfigByIdNotFound() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/compute-environment-configs/1") - .accept(APPLICATION_JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.empty()); - - mockMvc.perform(request).andExpect(status().isNotFound()); - - // Verify that the service was called - verify(mockComputeEnvironmentConfigService, times(1)).retrieve(any()); - } - - @Test - public void testCreateComputeEnvironmentConfig() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .post("/compute-environment-configs") - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(mapper.writeValueAsString(new ComputeEnvironmentConfig())) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request).andExpect(status().isCreated()); - - // Verify that the service was called - verify(mockComputeEnvironmentConfigService, times(1)).create(any()); - } - - @Test - public void testUpdateComputeEnvironmentConfig() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/compute-environment-configs/1") - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(mapper.writeValueAsString(computeEnvironmentConfig)) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); - - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify that the service was called - verify(mockComputeEnvironmentConfigService, times(1)).update(eq(computeEnvironmentConfig)); - } - - @Test(expected = NestedServletException.class) - public void testUpdateComputeEnvironmentConfig_IdMismatch() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/compute-environment-configs/2") - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(mapper.writeValueAsString(computeEnvironmentConfig)) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - // Throws NestedServletException in response to IllegalArgumentException - mockMvc.perform(request).andExpect(status().isBadRequest()); - - // Verify that the service was not called - verify(mockComputeEnvironmentConfigService, never()).update(any()); - } - - @Test - public void testDeleteComputeEnvironmentConfig() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete("/compute-environment-configs/1") - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request).andExpect(status().isNoContent()); - - // Verify that the service was called - verify(mockComputeEnvironmentConfigService, times(1)).delete(eq(1L)); - } - - @Test - public void testGetAvailableComputeEnvironmentConfigs() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/compute-environment-configs/available") - .param("user", mockUser.getLogin()) - .param("prj", "projectId") - .param("datatype", "xnat:mrSessionData") - .param("type", "JUPYTERHUB") - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify that the service was called - Map expectedScopeMap = new HashMap<>(); - expectedScopeMap.put(Scope.User, mockUser.getLogin()); - expectedScopeMap.put(Scope.Project, "projectId"); - expectedScopeMap.put(Scope.DataType, "xnat:mrSessionData"); - verify(mockComputeEnvironmentConfigService, times(1)).getAvailable(eq(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB), eq(expectedScopeMap)); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/rest/ConstraintConfigsApiTest.java b/src/test/java/org/nrg/xnat/compute/rest/ConstraintConfigsApiTest.java deleted file mode 100644 index 9a08639..0000000 --- a/src/test/java/org/nrg/xnat/compute/rest/ConstraintConfigsApiTest.java +++ /dev/null @@ -1,206 +0,0 @@ -package org.nrg.xnat.compute.rest; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.nrg.xnat.compute.config.ConstraintConfigsApiTestConfig; -import org.nrg.xnat.compute.models.ConstraintConfig; -import org.nrg.xnat.compute.services.ConstraintConfigService; -import org.nrg.xdat.security.services.RoleServiceI; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xft.security.UserI; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.util.NestedServletException; - -import java.util.Optional; - -import static org.mockito.Mockito.*; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebAppConfiguration -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {ConstraintConfigsApiTestConfig.class}) -public class ConstraintConfigsApiTest { - - @Autowired private WebApplicationContext wac; - @Autowired private ObjectMapper mapper; - @Autowired private RoleServiceI mockRoleService; - @Autowired private UserManagementServiceI mockUserManagementService; - @Autowired private ConstraintConfigService mockConstraintConfigService; - - private MockMvc mockMvc; - private UserI mockUser; - private Authentication mockAuthentication; - - private ConstraintConfig constraintConfig; - - @Before - public void before() { - mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); - - // Mock the user - mockUser = Mockito.mock(UserI.class); - when(mockUser.getLogin()).thenReturn("mockUser"); - when(mockUser.getEmail()).thenReturn("mockUser@mockuser.com"); - when(mockUser.getPassword()).thenReturn("mockUserPassword"); - when(mockUser.getID()).thenReturn(1); - when(mockRoleService.isSiteAdmin(mockUser)).thenReturn(true); - mockAuthentication = new TestingAuthenticationToken(mockUser, mockUser.getPassword()); - - // Set up a PlacementConstraintConfig - constraintConfig = new ConstraintConfig(); - constraintConfig.setId(1L); - } - - @After - public void after() { - Mockito.reset( - mockRoleService, - mockUserManagementService, - mockConstraintConfigService - ); - } - - @Test - public void testGetPlacementConstraintConfig() throws Exception { - // Set up the request - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/constraint-configs/1") - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - // Set up the mocks - when(mockConstraintConfigService.retrieve(1L)).thenReturn(Optional.of(constraintConfig)); - - // Make the call - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify the mocks - verify(mockConstraintConfigService).retrieve(1L); - } - - @Test - public void testGetPlacementConstraintConfigNotFound() throws Exception { - // Set up the request - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/constraint-configs/1") - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - // Set up the mocks - when(mockConstraintConfigService.retrieve(1L)).thenReturn(Optional.empty()); - - // Make the call - mockMvc.perform(request).andExpect(status().isNotFound()); - - // Verify the mocks - verify(mockConstraintConfigService).retrieve(1L); - } - - @Test - public void testGetAllPlacementConstraintConfigs() throws Exception { - // Set up the request - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/constraint-configs") - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - // Make the call - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify the mocks - verify(mockConstraintConfigService).getAll(); - } - - @Test - public void testCreatePlacementConstraintConfig() throws Exception { - // Set up the request - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .post("/constraint-configs") - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()) - .content(mapper.writeValueAsString(constraintConfig)); - - // Make the call - mockMvc.perform(request).andExpect(status().isCreated()); - - // Verify the mocks - verify(mockConstraintConfigService).create(constraintConfig); - } - - @Test - public void testUpdatePlacementConstraintConfig() throws Exception { - // Set up the request - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/constraint-configs/1") - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()) - .content(mapper.writeValueAsString(constraintConfig)); - - // Make the call - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify the mocks - verify(mockConstraintConfigService).update(constraintConfig); - } - - @Test(expected = NestedServletException.class) - public void testUpdatePlacementConstraintConfig_IdMismatch() throws Exception { - // Set up the request - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/constraint-configs/2") - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()) - .content(mapper.writeValueAsString(constraintConfig)); - - // Make the call - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify the mocks - verify(mockConstraintConfigService, never()).update(constraintConfig); - } - - @Test - public void testDeletePlacementConstraintConfig() throws Exception { - // Set up the request - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete("/constraint-configs/1") - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - // Make the call - mockMvc.perform(request).andExpect(status().isNoContent()); - - // Verify the mocks - verify(mockConstraintConfigService).delete(1L); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/rest/HardwareConfigsApiTest.java b/src/test/java/org/nrg/xnat/compute/rest/HardwareConfigsApiTest.java deleted file mode 100644 index 1348e96..0000000 --- a/src/test/java/org/nrg/xnat/compute/rest/HardwareConfigsApiTest.java +++ /dev/null @@ -1,201 +0,0 @@ -package org.nrg.xnat.compute.rest; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.nrg.xnat.compute.config.HardwareConfigsApiConfig; -import org.nrg.xnat.compute.models.HardwareConfig; -import org.nrg.xnat.compute.services.HardwareConfigService; -import org.nrg.xdat.security.services.RoleServiceI; -import org.nrg.xdat.security.services.UserManagementServiceI; -import org.nrg.xft.security.UserI; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.util.NestedServletException; - -import java.util.Optional; - -import static org.mockito.Mockito.*; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebAppConfiguration -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {HardwareConfigsApiConfig.class}) -public class HardwareConfigsApiTest { - - @Autowired private WebApplicationContext wac; - @Autowired private ObjectMapper mapper; - @Autowired private RoleServiceI mockRoleService; - @Autowired private UserManagementServiceI mockUserManagementService; - @Autowired private HardwareConfigService mockHardwareConfigService; - - private MockMvc mockMvc; - private UserI mockUser; - private Authentication mockAuthentication; - - private HardwareConfig hardwareConfig; - - @Before - public void before() throws Exception { - mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); - - // Mock the user - mockUser = Mockito.mock(UserI.class); - when(mockUser.getLogin()).thenReturn("mockUser"); - when(mockUser.getEmail()).thenReturn("mockUser@mockuser.com"); - when(mockUser.getPassword()).thenReturn("mockUserPassword"); - when(mockUser.getID()).thenReturn(1); - when(mockRoleService.isSiteAdmin(mockUser)).thenReturn(true); - mockAuthentication = new TestingAuthenticationToken(mockUser, mockUser.getPassword()); - - // Set up a hardware config - hardwareConfig = new HardwareConfig(); - hardwareConfig.setId(1L); - } - - @After - public void after() throws Exception { - // Reset the mock - Mockito.reset( - mockRoleService, - mockUserManagementService, - mockHardwareConfigService, - mockUser - ); - } - - @Test - public void testGetAllHardwareConfigs() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/hardware-configs") - .accept(APPLICATION_JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify that the service was called - verify(mockHardwareConfigService, times(1)).retrieveAll(); - } - - @Test - public void testGetHardwareConfigById() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/hardware-configs/1") - .accept(APPLICATION_JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - when(mockHardwareConfigService.retrieve(1L)).thenReturn(Optional.of(hardwareConfig)); - - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify that the service was called - verify(mockHardwareConfigService, times(1)).retrieve(1L); - } - - @Test - public void testGetHardwareConfigByIdNotFound() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get("/hardware-configs/1") - .accept(APPLICATION_JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - when(mockHardwareConfigService.retrieve(1L)).thenReturn(Optional.empty()); - - mockMvc.perform(request).andExpect(status().isNotFound()); - - // Verify that the service was called - verify(mockHardwareConfigService, times(1)).retrieve(1L); - } - - @Test - public void testCreateHardwareConfig() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .post("/hardware-configs") - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(mapper.writeValueAsString(hardwareConfig)) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - when(mockHardwareConfigService.create(hardwareConfig)).thenReturn(hardwareConfig); - - mockMvc.perform(request).andExpect(status().isCreated()); - - // Verify that the service was called - verify(mockHardwareConfigService, times(1)).create(hardwareConfig); - } - - @Test - public void testUpdateHardwareConfig() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/hardware-configs/1") - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(mapper.writeValueAsString(hardwareConfig)) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - when(mockHardwareConfigService.update(hardwareConfig)).thenReturn(hardwareConfig); - - mockMvc.perform(request).andExpect(status().isOk()); - - // Verify that the service was called - verify(mockHardwareConfigService, times(1)).update(hardwareConfig); - } - - @Test(expected = NestedServletException.class) - public void testUpdateHardwareConfig_IdMismatch() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put("/hardware-configs/2") - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(mapper.writeValueAsString(hardwareConfig)) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request).andExpect(status().isBadRequest()); - - // Verify that the service was not called - verify(mockHardwareConfigService, never()).update(hardwareConfig); - } - - @Test - public void testDeleteHardwareConfig() throws Exception { - final MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete("/hardware-configs/1") - .accept(APPLICATION_JSON) - .with(authentication(mockAuthentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request).andExpect(status().isNoContent()); - - // Verify that the service was called - verify(mockHardwareConfigService, times(1)).delete(1L); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigServiceTest.java deleted file mode 100644 index 90e271a..0000000 --- a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultComputeEnvironmentConfigServiceTest.java +++ /dev/null @@ -1,875 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.nrg.xnat.compute.config.DefaultComputeEnvironmentConfigServiceTestConfig; -import org.nrg.xnat.compute.models.*; -import org.nrg.framework.constants.Scope; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.services.HardwareConfigEntityService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import javax.transaction.Transactional; -import java.util.*; - -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.*; -import static org.nrg.framework.constants.Scope.*; -import static org.nrg.xnat.compute.models.ComputeEnvironmentConfig.ConfigType.CONTAINER_SERVICE; -import static org.nrg.xnat.compute.models.ComputeEnvironmentConfig.ConfigType.JUPYTERHUB; -import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; - -@RunWith(SpringJUnit4ClassRunner.class) -@Transactional -@ContextConfiguration(classes = DefaultComputeEnvironmentConfigServiceTestConfig.class) -public class DefaultComputeEnvironmentConfigServiceTest { - - @Autowired private DefaultComputeEnvironmentConfigService defaultComputeEnvironmentConfigService; - @Autowired private HardwareConfigEntityService hardwareConfigEntityService; - - private ComputeEnvironmentConfig computeEnvironmentConfig1; - private ComputeEnvironmentConfig computeEnvironmentConfig2; - private ComputeEnvironmentConfig computeEnvironmentConfig3; - private ComputeEnvironmentConfig computeEnvironmentConfigInvalid; - - private HardwareConfig hardwareConfig1; - private HardwareConfig hardwareConfig2; - private HardwareConfig hardwareConfig3; - - @Before - public void before() { - createDummyConfigs(); - } - - @Test - @DirtiesContext - public void testExists() { - // Test - ComputeEnvironmentConfig created = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - boolean exists = defaultComputeEnvironmentConfigService.exists(created.getId()); - - // Verify - assertTrue(exists); - } - - @Test - @DirtiesContext - public void testDoesNotExist() { - // Test - ComputeEnvironmentConfig created = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - boolean exists = defaultComputeEnvironmentConfigService.exists(created.getId() + 1); - - // Verify - assertFalse(exists); - } - - @Test - @DirtiesContext - public void testGetDoesNotExist() { - // Test - Optional computeEnvironmentConfig = defaultComputeEnvironmentConfigService.retrieve(1L); - - // Verify - assertFalse(computeEnvironmentConfig.isPresent()); - } - - @Test - @DirtiesContext - public void testGet() { - // Test - ComputeEnvironmentConfig created = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - Optional computeEnvironmentConfig = defaultComputeEnvironmentConfigService.retrieve(created.getId()); - - // Verify - assertTrue(computeEnvironmentConfig.isPresent()); - assertEquals(created, computeEnvironmentConfig.get()); - - assertEquals(computeEnvironmentConfig1.getComputeEnvironment(), computeEnvironmentConfig.get().getComputeEnvironment()); - assertEquals(computeEnvironmentConfig1.getConfigTypes(), computeEnvironmentConfig.get().getConfigTypes()); - assertEquals(computeEnvironmentConfig1.getScopes(), computeEnvironmentConfig.get().getScopes()); - } - - @Test - @DirtiesContext - public void testGetAll() { - // Test - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); - commitTransaction(); - ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); - commitTransaction(); - - List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAll(); - - // Verify - assertThat(computeEnvironmentConfigs.size(), is(3)); - assertThat(computeEnvironmentConfigs, hasItems(created1, created2, created3)); - } - - @Test - @DirtiesContext - public void testGetByType() { - // Create ComputeEnvironmentConfigs - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); - commitTransaction(); - ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); - commitTransaction(); - - // Test - List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getByType(CONTAINER_SERVICE); - - // Verify - assertThat(computeEnvironmentConfigs.size(), is(2)); - assertThat(computeEnvironmentConfigs, hasItems(created2, created3)); - - // Test - computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getByType(JUPYTERHUB); - - // Verify - assertEquals(2, computeEnvironmentConfigs.size()); - assertThat(computeEnvironmentConfigs, containsInAnyOrder(created1, created3)); - } - - @Test - @DirtiesContext - public void testGetAvailable_WrongUser() { - // Create ComputeEnvironmentConfigs - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); - commitTransaction(); - ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); - commitTransaction(); - - // Test - Map executionScope = new HashMap<>(); - executionScope.put(User, "User2"); - executionScope.put(Project, "Project1"); - executionScope.put(Experiment, "XNAT_E00001"); - executionScope.put(DataType, "xnat:petSessionData"); - List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable(null, executionScope); - - // Verify - assertThat(computeEnvironmentConfigs.size(), is(1)); - assertThat(computeEnvironmentConfigs, hasItems(created1)); - } - - @Test - @DirtiesContext - public void testGetAvailable_WrongProject() { - // Create ComputeEnvironmentConfigs - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); - commitTransaction(); - ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); - commitTransaction(); - - // Test - Map executionScope = new HashMap<>(); - executionScope.put(User, "User1"); - executionScope.put(Project, "Project2"); - executionScope.put(Experiment, "XNAT_E00001"); - executionScope.put(DataType, "xnat:petSessionData"); - List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable(executionScope); - - // Verify - assertThat(computeEnvironmentConfigs.size(), is(1)); - assertThat(computeEnvironmentConfigs, hasItem(created1)); - } - - @Test - @DirtiesContext - public void testGetAvailable() { - // Create hardware configs - HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); - HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); - HardwareConfigEntity hardwareConfigEntity3 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig3)); - commitTransaction(); - - hardwareConfig1.setId(hardwareConfigEntity1.getId()); - hardwareConfig2.setId(hardwareConfigEntity2.getId()); - hardwareConfig3.setId(hardwareConfigEntity3.getId()); - - // Create ComputeEnvironmentConfigs - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); - commitTransaction(); - ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); - commitTransaction(); - - // Test - Map executionScope = new HashMap<>(); - executionScope.put(User, "User1"); - executionScope.put(Project, "Project1"); - executionScope.put(Experiment, "XNAT_E00001"); - executionScope.put(DataType, "xnat:ctSessionData"); - List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable(executionScope); - - // Verify - assertThat(computeEnvironmentConfigs.size(), is(2)); - assertThat(computeEnvironmentConfigs.get(0).getHardwareOptions().getHardwareConfigs().size(), is(2)); - assertThat(computeEnvironmentConfigs.get(0).getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2)); - assertThat(computeEnvironmentConfigs.get(0).getHardwareOptions().getHardwareConfigs(), not(hasItems(hardwareConfig3))); - assertThat(computeEnvironmentConfigs.get(1).getHardwareOptions().getHardwareConfigs().size(), is(1)); - assertThat(computeEnvironmentConfigs.get(1).getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig2)); - } - - @Test - @DirtiesContext - public void testGetAvailable_SpecificType() { - // Create ComputeEnvironmentConfigs - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); - commitTransaction(); - ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); - commitTransaction(); - - // Test - Map executionScope = new HashMap<>(); - executionScope.put(User, "User1"); - executionScope.put(Project, "Project1"); - executionScope.put(Experiment, "XNAT_E00001"); - executionScope.put(DataType, "xnat:petSessionData"); - List computeEnvironmentConfigs = defaultComputeEnvironmentConfigService.getAvailable(CONTAINER_SERVICE, executionScope); - - // Verify - assertThat(computeEnvironmentConfigs.size(), is(1)); - assertThat(computeEnvironmentConfigs, hasItems(created2)); - } - - @Test - @DirtiesContext - public void testIsAvailable() { - // Create ComputeEnvironmentConfigs - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); - commitTransaction(); - ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); - commitTransaction(); - - // Test - Map executionScope = new HashMap<>(); - executionScope.put(User, "User1"); - executionScope.put(Project, "Project1"); - executionScope.put(Experiment, "XNAT_E00001"); - executionScope.put(DataType, "xnat:petSessionData"); - boolean result = defaultComputeEnvironmentConfigService.isAvailable(created1.getId(), executionScope); - - // Verify - assertTrue(result); - - // Test - result = defaultComputeEnvironmentConfigService.isAvailable(created2.getId(), executionScope); - - // Verify - assertTrue(result); - - // Test - result = defaultComputeEnvironmentConfigService.isAvailable(created3.getId(), executionScope); - - // Verify - assertFalse(result); - } - - @Test - @DirtiesContext - public void testNotAvailable_WrongUser() { - // Create ComputeEnvironmentConfigs - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); - commitTransaction(); - ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); - commitTransaction(); - - // Test - Map executionScope = new HashMap<>(); - executionScope.put(User, "User2"); - executionScope.put(Project, "Project1"); - executionScope.put(Experiment, "XNAT_E00001"); - executionScope.put(DataType, "xnat:petSessionData"); - boolean result = defaultComputeEnvironmentConfigService.isAvailable(created1.getId(), executionScope); - - // Verify - assertTrue(result); - - // Test - result = defaultComputeEnvironmentConfigService.isAvailable(created2.getId(), executionScope); - - // Verify - assertFalse(result); - - // Test - result = defaultComputeEnvironmentConfigService.isAvailable(created3.getId(), executionScope); - - // Verify - assertFalse(result); - } - - @Test - @DirtiesContext - public void testNotAvailable_WrongProject() { - // Create ComputeEnvironmentConfigs - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); - commitTransaction(); - ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); - commitTransaction(); - - // Test - Map executionScope = new HashMap<>(); - executionScope.put(User, "User1"); - executionScope.put(Project, "Project2"); - executionScope.put(Experiment, "XNAT_E00001"); - executionScope.put(DataType, "xnat:petSessionData"); - boolean result = defaultComputeEnvironmentConfigService.isAvailable(created1.getId(), executionScope); - - // Verify - assertTrue(result); - - // Test - result = defaultComputeEnvironmentConfigService.isAvailable(created2.getId(), executionScope); - - // Verify - assertFalse(result); - - // Test - result = defaultComputeEnvironmentConfigService.isAvailable(created3.getId(), executionScope); - - // Verify - assertFalse(result); - } - - @Test - @DirtiesContext - public void testNotAvailable_WrongDataType() { - // Create ComputeEnvironmentConfigs - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); - commitTransaction(); - ComputeEnvironmentConfig created3 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig3); - commitTransaction(); - - // Test - Map executionScope = new HashMap<>(); - executionScope.put(User, "User1"); - executionScope.put(Project, "Project1"); - executionScope.put(Experiment, "XNAT_E00001"); - executionScope.put(DataType, "xnat:projectData"); // Wrong data type - boolean result = defaultComputeEnvironmentConfigService.isAvailable(created1.getId(), executionScope); - - // Verify - assertTrue(result); - - // Test - result = defaultComputeEnvironmentConfigService.isAvailable(created2.getId(), executionScope); - - // Verify - assertFalse(result); - - // Test - result = defaultComputeEnvironmentConfigService.isAvailable(created3.getId(), executionScope); - - // Verify - assertFalse(result); - } - - @Test - @DirtiesContext - public void testCreate_AllowAllHardware() { - // First create hardware configs - HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); - HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); - HardwareConfigEntity hardwareConfigEntity3 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig3)); - commitTransaction(); - - hardwareConfig1.setId(hardwareConfigEntity1.getId()); - hardwareConfig2.setId(hardwareConfigEntity2.getId()); - hardwareConfig3.setId(hardwareConfigEntity3.getId()); - - // Next create compute environment config - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - - // Verify that all hardware configs are associated with the compute environment config - assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(3)); - assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2, hardwareConfig3)); - } - - @Test - @DirtiesContext - public void testCreate_SelectHardware() { - // First create hardware configs - HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); - HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); - HardwareConfigEntity hardwareConfigEntity3 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig3)); - commitTransaction(); - - hardwareConfig1.setId(hardwareConfigEntity1.getId()); - hardwareConfig2.setId(hardwareConfigEntity2.getId()); - hardwareConfig3.setId(hardwareConfigEntity3.getId()); - - // Next create compute environment config - ComputeEnvironmentConfig created2 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig2); - commitTransaction(); - - // Verify that only the selected hardware config is associated with the compute environment config - assertThat(created2.getHardwareOptions().getHardwareConfigs().size(), is(2)); - assertThat(created2.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig2, hardwareConfig3)); - } - - @Test - @DirtiesContext - public void testUpdate() throws NotFoundException { - // First create hardware configs - HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); - HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); - commitTransaction(); - - hardwareConfig1.setId(hardwareConfigEntity1.getId()); - hardwareConfig2.setId(hardwareConfigEntity2.getId()); - - // Next create compute environment config - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - - // Verify that all hardware configs are associated with the compute environment config - assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(2)); - assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2)); - - // Update the compute environment config - created1.getHardwareOptions().setAllowAllHardware(false); - created1.getHardwareOptions().getHardwareConfigs().remove(hardwareConfig1); - - // Update other fields - created1.getComputeEnvironment().setName("UpdatedName"); - created1.getComputeEnvironment().setImage("UpdatedImage"); - created1.getComputeEnvironment().getEnvironmentVariables().add(new EnvironmentVariable("UpdatedKey", "UpdatedValue")); - - // Update the compute environment config - defaultComputeEnvironmentConfigService.update(created1); - commitTransaction(); - - // Verify that only the selected hardware config is associated with the compute environment config - assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(1)); - assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItem(hardwareConfig2)); - - // Verify that the other fields were updated - assertThat(created1.getComputeEnvironment().getName(), is("UpdatedName")); - assertThat(created1.getComputeEnvironment().getImage(), is("UpdatedImage")); - assertThat(created1.getComputeEnvironment().getEnvironmentVariables(), hasItem(new EnvironmentVariable("UpdatedKey", "UpdatedValue"))); - } - - @Test - @DirtiesContext - public void testDelete() throws Exception { - // First create hardware configs - HardwareConfigEntity hardwareConfigEntity1 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig1)); - HardwareConfigEntity hardwareConfigEntity2 = hardwareConfigEntityService.create(HardwareConfigEntity.fromPojo(hardwareConfig2)); - commitTransaction(); - - hardwareConfig1.setId(hardwareConfigEntity1.getId()); - hardwareConfig2.setId(hardwareConfigEntity2.getId()); - - // Next create compute environment config - ComputeEnvironmentConfig created1 = defaultComputeEnvironmentConfigService.create(computeEnvironmentConfig1); - commitTransaction(); - - // Verify that all hardware configs are associated with the compute environment config - assertThat(created1.getHardwareOptions().getHardwareConfigs().size(), is(2)); - assertThat(created1.getHardwareOptions().getHardwareConfigs(), hasItems(hardwareConfig1, hardwareConfig2)); - - hardwareConfigEntity1 = hardwareConfigEntityService.retrieve(hardwareConfigEntity1.getId()); - hardwareConfigEntity2 = hardwareConfigEntityService.retrieve(hardwareConfigEntity2.getId()); - - assertThat(hardwareConfigEntity1.getComputeEnvironmentHardwareOptions().size(), is(1)); - assertThat(hardwareConfigEntity2.getComputeEnvironmentHardwareOptions().size(), is(1)); - - // Delete the compute environment config - defaultComputeEnvironmentConfigService.delete(created1.getId()); - commitTransaction(); - - // Verify that the compute environment config was deleted - assertThat(defaultComputeEnvironmentConfigService.exists(created1.getId()), is(false)); - - // Verify that the hardware config entities were deleted - hardwareConfigEntity1 = hardwareConfigEntityService.retrieve(hardwareConfigEntity1.getId()); - hardwareConfigEntity2 = hardwareConfigEntityService.retrieve(hardwareConfigEntity2.getId()); - - assertThat(hardwareConfigEntity1.getComputeEnvironmentHardwareOptions().size(), is(0)); - assertThat(hardwareConfigEntity2.getComputeEnvironmentHardwareOptions().size(), is(0)); - } - - @Test - public void testValidate() { - try { - defaultComputeEnvironmentConfigService.validate(computeEnvironmentConfigInvalid); - fail("Expected exception to be thrown"); - } catch (IllegalArgumentException e) { - // Verify that the exception message contains the expected validation errors - // Note: the order of the validation errors is not guaranteed - // Trying not to be too brittle here - assertThat(e.getMessage(), containsString("name cannot be blank")); - assertThat(e.getMessage(), containsString("image cannot be blank")); - assertThat(e.getMessage(), containsString("must have at least one scope")); - assertThat(e.getMessage(), containsString("hardware configs cannot be null")); - assertThat(e.getMessage(), containsString("must have at least one config type")); - } - } - - public void createDummyConfigs() { - // Setup hardware - Hardware hardware1 = Hardware.builder() - .name("Small") - .cpuReservation(2.0) - .cpuLimit(4.0) - .memoryReservation("4G") - .memoryLimit("8G") - .build(); - - // Setup hardware constraints - Constraint hardwareConstraint1 = Constraint.builder() - .key("node.role") - .operator(Constraint.Operator.IN) - .values(new HashSet<>(Collections.singletonList("worker"))) - .build(); - - Constraint hardwareConstraint2 = Constraint.builder() - .key("node.instance.type") - .operator(Constraint.Operator.NOT_IN) - .values(new HashSet<>(Arrays.asList("spot", "demand"))) - .build(); - - hardware1.setConstraints(Arrays.asList(hardwareConstraint1, hardwareConstraint2)); - - // Setup hardware environment variables - EnvironmentVariable hardwareEnvironmentVariable1 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); - EnvironmentVariable hardwareEnvironmentVariable2 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); - - hardware1.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable1, hardwareEnvironmentVariable2)); - - // Setup hardware generic resources - GenericResource hardwareGenericResource1 = new GenericResource("nvidia.com/gpu", "2"); - GenericResource hardwareGenericResource2 = new GenericResource("fpga.com/fpga", "1"); - - hardware1.setGenericResources(Arrays.asList(hardwareGenericResource1, hardwareGenericResource2)); - - // Setup hardware scopes - HardwareScope hardwareSiteScope1 = HardwareScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope hardwareProjectScope1 = HardwareScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope userHardwareScope1 = HardwareScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map hardwareScopes1 = new HashMap<>(); - hardwareScopes1.put(Site, hardwareSiteScope1); - hardwareScopes1.put(Project, hardwareProjectScope1); - hardwareScopes1.put(User, userHardwareScope1); - - // Setup a hardware config entity - hardwareConfig1 = HardwareConfig.builder() - .hardware(hardware1) - .scopes(hardwareScopes1) - .build(); - - // Setup second hardware config - Hardware hardware2 = Hardware.builder() - .name("Medium") - .cpuReservation(4.0) - .cpuLimit(4.0) - .memoryReservation("8G") - .memoryLimit("8G") - .build(); - - // Setup hardware constraints - Constraint hardwareConstraint3 = Constraint.builder() - .key("node.role") - .operator(Constraint.Operator.IN) - .values(new HashSet<>(Collections.singletonList("worker"))) - .build(); - - Constraint hardwareConstraint4 = Constraint.builder() - .key("node.instance.type") - .operator(Constraint.Operator.NOT_IN) - .values(new HashSet<>(Arrays.asList("spot", "demand"))) - .build(); - - hardware2.setConstraints(Arrays.asList(hardwareConstraint3, hardwareConstraint4)); - - // Setup hardware environment variables - EnvironmentVariable hardwareEnvironmentVariable3 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); - EnvironmentVariable hardwareEnvironmentVariable4 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); - - hardware2.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable3, hardwareEnvironmentVariable4)); - - // Setup hardware generic resources - GenericResource hardwareGenericResource3 = new GenericResource("nvidia.com/gpu", "2"); - GenericResource hardwareGenericResource4 = new GenericResource("fpga.com/fpga", "1"); - - hardware2.setGenericResources(Arrays.asList(hardwareGenericResource3, hardwareGenericResource4)); - - // Setup hardware scopes - HardwareScope hardwareSiteScope2 = HardwareScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope hardwareProjectScope2 = HardwareScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope userHardwareScope2 = HardwareScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map hardwareScopes2 = new HashMap<>(); - hardwareScopes2.put(Site, hardwareSiteScope2); - hardwareScopes2.put(Project, hardwareProjectScope2); - hardwareScopes2.put(User, userHardwareScope2); - - // Setup second hardware config entity - hardwareConfig2 = HardwareConfig.builder() - .hardware(hardware2) - .scopes(hardwareScopes2) - .build(); - - // Setup second hardware config - Hardware hardware3 = Hardware.builder() - .name("Large") - .cpuReservation(8.0) - .cpuLimit(8.0) - .memoryReservation("16G") - .memoryLimit("16G") - .build(); - - // No constraints, environment variables or generic resources - hardware3.setConstraints(Collections.emptyList()); - hardware3.setEnvironmentVariables(Collections.emptyList()); - hardware3.setGenericResources(Collections.emptyList()); - - // Setup hardware scopes - HardwareScope hardwareSiteScope3 = HardwareScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope hardwareProjectScope3 = HardwareScope.builder() - .scope(Project) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("ProjectABCDE"))) - .build(); - - HardwareScope userHardwareScope3 = HardwareScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map hardwareScopes3 = new HashMap<>(); - hardwareScopes3.put(Site, hardwareSiteScope3); - hardwareScopes3.put(Project, hardwareProjectScope3); - hardwareScopes3.put(User, userHardwareScope3); - - // Setup second hardware config entity - hardwareConfig3 = HardwareConfig.builder() - .hardware(hardware3) - .scopes(hardwareScopes3) - .build(); - - // Setup first compute environment - ComputeEnvironment computeEnvironment1 = ComputeEnvironment.builder() - .name("Jupyter Datascience Notebook") - .image("jupyter/datascience-notebook:hub-3.0.0") - .environmentVariables(new ArrayList<>()) - .mounts(new ArrayList<>()) - .build(); - - ComputeEnvironmentScope computeEnvironmentSiteScope1 = ComputeEnvironmentScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeEnvironmentScope computeEnvironmentProjectScope1 = ComputeEnvironmentScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeEnvironmentScope computeEnvironmentUserScope1 = ComputeEnvironmentScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map computeEnvironmentScopes1 = new HashMap<>(); - computeEnvironmentScopes1.put(Site, computeEnvironmentSiteScope1); - computeEnvironmentScopes1.put(Project, computeEnvironmentProjectScope1); - computeEnvironmentScopes1.put(User, computeEnvironmentUserScope1); - - ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions1 = ComputeEnvironmentHardwareOptions.builder() - .allowAllHardware(true) - .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig1, hardwareConfig2, hardwareConfig3))) - .build(); - - computeEnvironmentConfig1 = ComputeEnvironmentConfig.builder() - .configTypes(new HashSet<>(Collections.singletonList(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB))) - .computeEnvironment(computeEnvironment1) - .scopes(computeEnvironmentScopes1) - .hardwareOptions(computeEnvironmentHardwareOptions1) - .build(); - - // Setup second compute environment - ComputeEnvironment computeEnvironment2 = ComputeEnvironment.builder() - .name("XNAT Datascience Notebook") - .image("xnat/datascience-notebook:latest") - .environmentVariables(new ArrayList<>()) - .mounts(new ArrayList<>()) - .build(); - - ComputeEnvironmentScope computeEnvironmentSiteScope2 = ComputeEnvironmentScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeEnvironmentScope computeEnvironmentProjectScope2 = ComputeEnvironmentScope.builder() - .scope(Project) - .enabled(false) - .ids(new HashSet<>(new ArrayList<>(Arrays.asList("Project1", "Project10", "Project100")))) - .build(); - - ComputeEnvironmentScope computeEnvironmentUserScope2 = ComputeEnvironmentScope.builder() - .scope(User) - .enabled(false) - .ids(new HashSet<>(new ArrayList<>(Arrays.asList("User1", "User10", "User100")))) - .build(); - - ComputeEnvironmentScope computeEnvironmentDatatypeScope2 = ComputeEnvironmentScope.builder() - .scope(DataType) - .enabled(false) - .ids(new HashSet<>(Arrays.asList("xnat:mrSessionData", "xnat:petSessionData", "xnat:ctSessionData"))) - .build(); - - Map computeEnvironmentScopes2 = new HashMap<>(); - computeEnvironmentScopes2.put(Site, computeEnvironmentSiteScope2); - computeEnvironmentScopes2.put(Project, computeEnvironmentProjectScope2); - computeEnvironmentScopes2.put(User, computeEnvironmentUserScope2); - computeEnvironmentScopes2.put(DataType, computeEnvironmentDatatypeScope2); - - ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions2 = ComputeEnvironmentHardwareOptions.builder() - .allowAllHardware(false) - .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig2, hardwareConfig3))) - .build(); - - computeEnvironmentConfig2 = ComputeEnvironmentConfig.builder() - .id(2L) - .configTypes(new HashSet<>(Collections.singletonList(CONTAINER_SERVICE))) - .computeEnvironment(computeEnvironment2) - .scopes(computeEnvironmentScopes2) - .hardwareOptions(computeEnvironmentHardwareOptions2) - .build(); - - // Setup third compute environment - ComputeEnvironment computeEnvironment3 = ComputeEnvironment.builder() - .name("MATLAB Datascience Notebook") - .image("matlab/datascience-notebook:latest") - .environmentVariables(new ArrayList<>()) - .mounts(new ArrayList<>()) - .build(); - - ComputeEnvironmentScope computeEnvironmentSiteScope3 = ComputeEnvironmentScope.builder() - .scope(Site) - .enabled(false) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeEnvironmentScope computeEnvironmentProjectScope3 = ComputeEnvironmentScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>()) - .build(); - - ComputeEnvironmentScope computeEnvironmentUserScope3 = ComputeEnvironmentScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>()) - .build(); - - Map computeEnvironmentScopes3 = new HashMap<>(); - computeEnvironmentScopes3.put(Site, computeEnvironmentSiteScope3); - computeEnvironmentScopes3.put(Project, computeEnvironmentProjectScope3); - computeEnvironmentScopes3.put(User, computeEnvironmentUserScope3); - - ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions3 = ComputeEnvironmentHardwareOptions.builder() - .allowAllHardware(true) - .hardwareConfigs(new HashSet<>()) - .build(); - - computeEnvironmentConfig3 = ComputeEnvironmentConfig.builder() - .configTypes(new HashSet<>(Arrays.asList(CONTAINER_SERVICE, JUPYTERHUB))) - .computeEnvironment(computeEnvironment3) - .scopes(computeEnvironmentScopes3) - .hardwareOptions(computeEnvironmentHardwareOptions3) - .build(); - - // Setup invalid compute environment config - ComputeEnvironment computeEnvironmentInvalid = ComputeEnvironment.builder() - .name("") // invalid - .image("") // invalid - .environmentVariables(new ArrayList<>()) - .mounts(new ArrayList<>()) - .build(); - - Map computeEnvironmentScopesInvalid = new HashMap<>(); // invalid, no scopes - - ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptionsInvalid = ComputeEnvironmentHardwareOptions.builder() - .allowAllHardware(true) - .hardwareConfigs(null) // invalid - .build(); - - computeEnvironmentConfigInvalid = ComputeEnvironmentConfig.builder() - .configTypes(null) // invalid - .computeEnvironment(computeEnvironmentInvalid) - .scopes(computeEnvironmentScopesInvalid) - .hardwareOptions(computeEnvironmentHardwareOptionsInvalid) - .build(); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigServiceTest.java deleted file mode 100644 index 5af7261..0000000 --- a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultConstraintConfigServiceTest.java +++ /dev/null @@ -1,278 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.nrg.framework.constants.Scope; -import org.nrg.framework.exceptions.NotFoundException; -import org.nrg.xnat.compute.config.DefaultConstraintConfigServiceTestConfig; -import org.nrg.xnat.compute.models.Constraint; -import org.nrg.xnat.compute.models.ConstraintConfig; -import org.nrg.xnat.compute.models.ConstraintScope; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import javax.transaction.Transactional; -import java.util.*; - -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; -import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; - -@RunWith(SpringJUnit4ClassRunner.class) -@Transactional -@ContextConfiguration(classes = DefaultConstraintConfigServiceTestConfig.class) -public class DefaultConstraintConfigServiceTest { - - @Autowired private DefaultConstraintConfigService constraintConfigService; - - private ConstraintConfig constraintConfig1; - private ConstraintConfig constraintConfig2; - private ConstraintConfig constraintConfig3; - private ConstraintConfig constraintConfigInvalid; - - @Before - public void before() { - createDummyConstraintConfigs(); - } - - @Test - @DirtiesContext - public void testRetrieve() { - // Test - ConstraintConfig created = constraintConfigService.create(constraintConfig1); - commitTransaction(); - Optional retrieved = constraintConfigService.retrieve(created.getId()); - - // Verify - assertTrue(retrieved.isPresent()); - assertEquals(created, retrieved.get()); - - constraintConfig1.setId(created.getId()); - assertThat(retrieved.get(), is(constraintConfig1)); - } - - @Test - @DirtiesContext - public void testRetrieveDoesNotExist() { - // Test - Optional retrieved = constraintConfigService.retrieve(1L); - - // Verify - assertFalse(retrieved.isPresent()); - } - - @Test - @DirtiesContext - public void testGetAll() { - // Test - ConstraintConfig created1 = constraintConfigService.create(constraintConfig1); - commitTransaction(); - ConstraintConfig created2 = constraintConfigService.create(constraintConfig2); - commitTransaction(); - ConstraintConfig created3 = constraintConfigService.create(constraintConfig3); - commitTransaction(); - - List retrieved = constraintConfigService.getAll(); - - // Verify - assertEquals(3, retrieved.size()); - assertThat(retrieved, hasItems(created1, created2, created3)); - } - - @Test - @DirtiesContext - public void testCreate() { - // Test - ConstraintConfig created = constraintConfigService.create(constraintConfig1); - commitTransaction(); - - // Verify - assertNotNull(created.getId()); - constraintConfig1.setId(created.getId()); - assertEquals(created, constraintConfig1); - } - - @Test - @DirtiesContext - public void testUpdate() throws NotFoundException { - // Test - ConstraintConfig created = constraintConfigService.create(constraintConfig1); - commitTransaction(); - created.getConstraint().setKey("newKey"); - ConstraintConfig updated = constraintConfigService.update(created); - commitTransaction(); - - // Verify - assertEquals(created, updated); - } - - @Test - @DirtiesContext - public void testDelete() { - // Test - ConstraintConfig created = constraintConfigService.create(constraintConfig1); - commitTransaction(); - constraintConfigService.delete(created.getId()); - commitTransaction(); - - // Verify - Optional retrieved = constraintConfigService.retrieve(created.getId()); - assertFalse(retrieved.isPresent()); - } - - @Test - @DirtiesContext - public void testGetAvailable() { - // Setup - ConstraintConfig created1 = constraintConfigService.create(constraintConfig1); - commitTransaction(); - ConstraintConfig created2 = constraintConfigService.create(constraintConfig2); - commitTransaction(); - ConstraintConfig created3 = constraintConfigService.create(constraintConfig3); - commitTransaction(); - - // Test - Map executionScopes = new HashMap<>(); - executionScopes.put(Scope.Project, "ProjectA"); - List retrieved = constraintConfigService.getAvailable(executionScopes); - - // Verify - assertEquals(2, retrieved.size()); - assertThat(retrieved, hasItems(created1, created3)); - - // Test - executionScopes.put(Scope.Project, "ProjectB"); - retrieved = constraintConfigService.getAvailable(executionScopes); - - // Verify - assertEquals(2, retrieved.size()); - assertThat(retrieved, hasItems(created1, created3)); - - // Test - executionScopes.put(Scope.Project, "ProjectC"); - retrieved = constraintConfigService.getAvailable(executionScopes); - - // Verify - assertEquals(1, retrieved.size()); - assertThat(retrieved, hasItem(created1)); - } - - @Test - public void testValidate() { - try { - constraintConfigService.validate(constraintConfigInvalid); - fail("Should have thrown an exception"); - } catch (Exception e) { - assertThat(e.getMessage(), containsString("key cannot be null or blank")); - assertThat(e.getMessage(), containsString("values cannot be null or empty")); - assertThat(e.getMessage(), containsString("operator cannot be null")); - assertThat(e.getMessage(), containsString("Scopes cannot be null or empty")); - } - } - - protected void createDummyConstraintConfigs() { - createDummyConstraintConfig1(); - createDummyConstraintConfig2(); - createDummyConstraintConfig3(); - createDummyConstraintConfigInvalid(); - } - - protected void createDummyConstraintConfig1() { - constraintConfig1 = new ConstraintConfig(); - - Constraint constraint1 = new Constraint(); - constraint1.setKey("node.role"); - constraint1.setOperator(Constraint.Operator.IN); - constraint1.setValues(new HashSet<>(Collections.singletonList("worker"))); - constraintConfig1.setConstraint(constraint1); - - ConstraintScope siteScope1 = ConstraintScope.builder() - .scope(Scope.Site) - .enabled(true) - .ids(Collections.emptySet()) - .build(); - ConstraintScope projectScope1 = ConstraintScope.builder() - .scope(Scope.Project) - .enabled(true) - .ids(Collections.emptySet()) - .build(); - - Map scopes1 = new HashMap<>(); - scopes1.put(Scope.Site, siteScope1); - scopes1.put(Scope.Project, projectScope1); - - constraintConfig1.setScopes(scopes1); - } - - protected void createDummyConstraintConfig2() { - constraintConfig2 = new ConstraintConfig(); - - Constraint constraint2 = new Constraint(); - constraint2.setKey("node.role"); - constraint2.setOperator(Constraint.Operator.IN); - constraint2.setValues(new HashSet<>(Collections.singletonList("worker"))); - constraintConfig2.setConstraint(constraint2); - - ConstraintScope siteScope2 = ConstraintScope.builder() - .scope(Scope.Site) - .enabled(false) - .ids(Collections.emptySet()) - .build(); - ConstraintScope projectScope2 = ConstraintScope.builder() - .scope(Scope.Project) - .enabled(true) - .ids(Collections.emptySet()) - .build(); - - Map scopes2 = new HashMap<>(); - scopes2.put(Scope.Site, siteScope2); - scopes2.put(Scope.Project, projectScope2); - - constraintConfig2.setScopes(scopes2); - } - - protected void createDummyConstraintConfig3() { - constraintConfig3 = new ConstraintConfig(); - - Constraint constraint3 = new Constraint(); - constraint3.setKey("node.label.projects"); - constraint3.setOperator(Constraint.Operator.IN); - constraint3.setValues(new HashSet<>(Arrays.asList("ProjectA", "ProjectB"))); - constraintConfig3.setConstraint(constraint3); - - ConstraintScope siteScope3 = ConstraintScope.builder() - .scope(Scope.Site) - .enabled(true) - .ids(Collections.emptySet()) - .build(); - ConstraintScope projectScope3 = ConstraintScope.builder() - .scope(Scope.Project) - .enabled(false) - .ids(new HashSet<>(Arrays.asList("ProjectA", "ProjectB"))) - .build(); - - Map scopes3 = new HashMap<>(); - scopes3.put(Scope.Site, siteScope3); - scopes3.put(Scope.Project, projectScope3); - - constraintConfig3.setScopes(scopes3); - } - - protected void createDummyConstraintConfigInvalid() { - constraintConfigInvalid = new ConstraintConfig(); - - Constraint constraintInvalid = new Constraint(); - constraintInvalid.setKey(""); - constraintInvalid.setOperator(null); - constraintInvalid.setValues(new HashSet<>()); - constraintConfigInvalid.setConstraint(constraintInvalid); - - Map scopesInvalid = new HashMap<>(); - - constraintConfigInvalid.setScopes(scopesInvalid); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java deleted file mode 100644 index e876dbd..0000000 --- a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultHardwareConfigServiceTest.java +++ /dev/null @@ -1,480 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.nrg.xnat.compute.models.*; -import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.config.DefaultHardwareConfigServiceTestConfig; -import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import javax.transaction.Transactional; -import java.util.*; -import java.util.stream.Collectors; - -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; -import static org.nrg.framework.constants.Scope.*; -import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; - -@RunWith(SpringJUnit4ClassRunner.class) -@Transactional -@ContextConfiguration(classes = DefaultHardwareConfigServiceTestConfig.class) -public class DefaultHardwareConfigServiceTest { - - @Autowired private DefaultHardwareConfigService defaultHardwareConfigService; - @Autowired private ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService; - - private ComputeEnvironmentConfig computeEnvironmentConfig1; - private ComputeEnvironmentConfig computeEnvironmentConfig2; - private HardwareConfig hardwareConfig1; - private HardwareConfig hardwareConfig2; - private HardwareConfig hardwareConfigInvalid; - - @Before - public void before() throws Exception { - createDummyConfigsAndEntities(); - } - - @Test - @DirtiesContext - public void testExists() { - // Test - HardwareConfig hardwareConfig = defaultHardwareConfigService.create(hardwareConfig1); - commitTransaction(); - boolean exists = defaultHardwareConfigService.exists(hardwareConfig.getId()); - - // Verify - assertTrue(exists); - } - - @Test - @DirtiesContext - public void testDoesNotExist() { - // Test - boolean exists = defaultHardwareConfigService.exists(3L); - - // Verify - assertFalse(exists); - } - - @Test - @DirtiesContext - public void testRetrieve() { - // Setup - HardwareConfig created = defaultHardwareConfigService.create(hardwareConfig1); - commitTransaction(); - Optional retrieved = defaultHardwareConfigService.retrieve(created.getId()); - - // Verify - assertTrue(retrieved.isPresent()); - assertThat(retrieved.get(), is(created)); - - hardwareConfig1.setId(retrieved.get().getId()); - assertThat(retrieved.get(), is(hardwareConfig1)); - } - - @Test - @DirtiesContext - public void testRetrieve_DoesNotExist() { - // Setup - Optional retrieved = defaultHardwareConfigService.retrieve(3L); - - // Verify - assertFalse(retrieved.isPresent()); - } - - @Test - @DirtiesContext - public void testRetrieveAll() { - // Setup - HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); - HardwareConfig created2 = defaultHardwareConfigService.create(hardwareConfig2); - commitTransaction(); - - // Test - List retrieved = defaultHardwareConfigService.retrieveAll(); - - // Verify - assertEquals(2, retrieved.size()); - assertThat(retrieved, hasItems(created1, created2)); - } - - @Test - @DirtiesContext - public void testCreate() { - // First, create the compute environment configs - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity1 = computeEnvironmentConfigEntityService.create(ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig1)); - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity2 = computeEnvironmentConfigEntityService.create(ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig2)); - commitTransaction(); - - // Now create a hardware config - HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); - commitTransaction(); - HardwareConfig created2 = defaultHardwareConfigService.create(hardwareConfig2); - commitTransaction(); - - // Then retrieve the compute environment configs - computeEnvironmentConfigEntity1 = computeEnvironmentConfigEntityService.retrieve(computeEnvironmentConfigEntity1.getId()); - computeEnvironmentConfigEntity2 = computeEnvironmentConfigEntityService.retrieve(computeEnvironmentConfigEntity2.getId()); - - // Verify that the hardware config were added only to the first compute environment config - assertThat(computeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().size(), is(2)); - assertThat(computeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().size(), is(0)); - assertThat(computeEnvironmentConfigEntity1 - .getHardwareOptions() - .getHardwareConfigs().stream() - .map(HardwareConfigEntity::toPojo) - .collect(Collectors.toList()), - hasItems(created1, created2)); - } - - @Test - @DirtiesContext - public void testUpdate() throws Exception { - // Setup - HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); - commitTransaction(); - - // Test - created1.getHardware().setName("Updated"); - created1.getHardware().setCpuLimit(6.5); - created1.getHardware().setMemoryLimit("10G"); - - defaultHardwareConfigService.update(created1); - commitTransaction(); - - // Verify - HardwareConfig retrieved = defaultHardwareConfigService.retrieve(created1.getId()).get(); - - assertThat(retrieved, is(created1)); - assertThat(retrieved.getHardware().getName(), is("Updated")); - assertThat(retrieved.getHardware().getCpuLimit(), is(6.5)); - assertThat(retrieved.getHardware().getMemoryLimit(), is("10G")); - } - - @Test - @DirtiesContext - public void testDelete() throws Exception { - // Setup - HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); - commitTransaction(); - - // Test - defaultHardwareConfigService.delete(created1.getId()); - commitTransaction(); - - // Verify - assertFalse(defaultHardwareConfigService.exists(created1.getId())); - } - - @Test - @DirtiesContext - public void testIsAvailable() { - // Setup - HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); - HardwareConfig created2 = defaultHardwareConfigService.create(hardwareConfig2); - commitTransaction(); - - // Test - Map executionScope = new HashMap<>(); - executionScope.put(User, "User1"); - executionScope.put(Project, "Project1"); - boolean isAvailable1 = defaultHardwareConfigService.isAvailable(created1.getId(), executionScope); - boolean isAvailable2 = defaultHardwareConfigService.isAvailable(created2.getId(), executionScope); - - // Verify - assertTrue(isAvailable1); - assertTrue(isAvailable2); - } - - @Test - @DirtiesContext - public void testIsAvailable_WrongUser() { - // Setup - HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); - HardwareConfig created2 = defaultHardwareConfigService.create(hardwareConfig2); - commitTransaction(); - - // Test - Map executionScope = new HashMap<>(); - executionScope.put(User, "User2"); - executionScope.put(Project, "Project1"); - boolean isAvailable1 = defaultHardwareConfigService.isAvailable(created1.getId(), executionScope); - boolean isAvailable2 = defaultHardwareConfigService.isAvailable(created2.getId(), executionScope); - - // Verify - assertTrue(isAvailable1); - assertFalse(isAvailable2); - } - - @Test - @DirtiesContext - public void testIsAvailable_WrongProject() { - // Setup - HardwareConfig created1 = defaultHardwareConfigService.create(hardwareConfig1); - HardwareConfig created2 = defaultHardwareConfigService.create(hardwareConfig2); - commitTransaction(); - - // Test - Map executionScope = new HashMap<>(); - executionScope.put(User, "User1"); - executionScope.put(Project, "Project2"); - boolean isAvailable1 = defaultHardwareConfigService.isAvailable(created1.getId(), executionScope); - boolean isAvailable2 = defaultHardwareConfigService.isAvailable(created2.getId(), executionScope); - - // Verify - assertTrue(isAvailable1); - assertFalse(isAvailable2); - } - - @Test - public void testValidate() { - try { - defaultHardwareConfigService.validate(hardwareConfigInvalid); - fail("Expected an IllegalArgumentException to be thrown"); - } catch (IllegalArgumentException e) { - // Verify that the exception message contains the expected validation errors - // Note: the order of the validation errors is not guaranteed - // Trying not to be too brittle here - assertThat(e.getMessage(), containsString("name cannot be blank")); - assertThat(e.getMessage(), containsString("scopes cannot be null or empty")); - } - } - - public void createDummyConfigsAndEntities() { - // Setup hardware - Hardware hardware1 = Hardware.builder() - .name("Small") - .cpuReservation(2.0) - .cpuLimit(4.0) - .memoryReservation("4G") - .memoryLimit("8G") - .build(); - - // Setup hardware constraints - Constraint hardwareConstraint1 = Constraint.builder() - .key("node.role") - .operator(Constraint.Operator.IN) - .values(new HashSet<>(Collections.singletonList("worker"))) - .build(); - - Constraint hardwareConstraint2 = Constraint.builder() - .key("node.instance.type") - .operator(Constraint.Operator.NOT_IN) - .values(new HashSet<>(Arrays.asList("spot", "demand"))) - .build(); - - hardware1.setConstraints(Arrays.asList(hardwareConstraint1, hardwareConstraint2)); - - // Setup hardware environment variables - EnvironmentVariable hardwareEnvironmentVariable1 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); - EnvironmentVariable hardwareEnvironmentVariable2 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); - - hardware1.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable1, hardwareEnvironmentVariable2)); - - // Setup hardware generic resources - GenericResource hardwareGenericResource1 = new GenericResource("nvidia.com/gpu", "2"); - GenericResource hardwareGenericResource2 = new GenericResource("fpga.com/fpga", "1"); - - hardware1.setGenericResources(Arrays.asList(hardwareGenericResource1, hardwareGenericResource2)); - - // Setup hardware scopes - HardwareScope hardwareSiteScope1 = HardwareScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope hardwareProjectScope1 = HardwareScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope userHardwareScope1 = HardwareScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map hardwareScopes1 = new HashMap<>(); - hardwareScopes1.put(Site, hardwareSiteScope1); - hardwareScopes1.put(Project, hardwareProjectScope1); - hardwareScopes1.put(User, userHardwareScope1); - - // Build hardware config - hardwareConfig1 = HardwareConfig.builder() - .hardware(hardware1) - .scopes(hardwareScopes1) - .build(); - - // Setup second hardware config - Hardware hardware2 = Hardware.builder() - .name("Medium") - .cpuReservation(4.0) - .cpuLimit(4.0) - .memoryReservation("8G") - .memoryLimit("8G") - .build(); - - // Setup hardware constraints - Constraint hardwareConstraint3 = Constraint.builder() - .key("node.role") - .operator(Constraint.Operator.IN) - .values(new HashSet<>(Collections.singletonList("worker"))) - .build(); - - Constraint hardwareConstraint4 = Constraint.builder() - .key("node.instance.type") - .operator(Constraint.Operator.NOT_IN) - .values(new HashSet<>(Arrays.asList("spot", "demand"))) - .build(); - - hardware2.setConstraints(Arrays.asList(hardwareConstraint3, hardwareConstraint4)); - - // Setup hardware environment variables - EnvironmentVariable hardwareEnvironmentVariable3 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); - EnvironmentVariable hardwareEnvironmentVariable4 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); - - hardware2.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable3, hardwareEnvironmentVariable4)); - - // Setup hardware generic resources - GenericResource hardwareGenericResource3 = new GenericResource("nvidia.com/gpu", "2"); - GenericResource hardwareGenericResource4 = new GenericResource("fpga.com/fpga", "1"); - - hardware2.setGenericResources(Arrays.asList(hardwareGenericResource3, hardwareGenericResource4)); - - // Setup hardware scopes - HardwareScope hardwareSiteScope2 = HardwareScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope hardwareProjectScope2 = HardwareScope.builder() - .scope(Project) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("Project1"))) - .build(); - - HardwareScope userHardwareScope2 = HardwareScope.builder() - .scope(User) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("User1"))) - .build(); - - Map hardwareScopes2 = new HashMap<>(); - hardwareScopes2.put(Site, hardwareSiteScope2); - hardwareScopes2.put(Project, hardwareProjectScope2); - hardwareScopes2.put(User, userHardwareScope2); - - // Build second hardware config - hardwareConfig2 = HardwareConfig.builder() - .hardware(hardware2) - .scopes(hardwareScopes2) - .build(); - - // Setup invalid hardware config - Hardware hardwareInvalid = Hardware.builder().build(); - Map hardwareScopesInvalid = new HashMap<>(); - hardwareConfigInvalid = HardwareConfig.builder() - .hardware(hardwareInvalid) - .scopes(hardwareScopesInvalid) - .build(); - - // Setup first compute environment - ComputeEnvironment computeEnvironment1 = ComputeEnvironment.builder() - .name("Jupyter Datascience Notebook") - .image("jupyter/datascience-notebook:hub-3.0.0") - .environmentVariables(new ArrayList<>()) - .mounts(new ArrayList<>()) - .build(); - - ComputeEnvironmentScope computeEnvironmentSiteScope1 = ComputeEnvironmentScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeEnvironmentScope computeEnvironmentProjectScope1 = ComputeEnvironmentScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeEnvironmentScope computeEnvironmentUserScope1 = ComputeEnvironmentScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map computeEnvironmentScopes1 = new HashMap<>(); - computeEnvironmentScopes1.put(Site, computeEnvironmentSiteScope1); - computeEnvironmentScopes1.put(Project, computeEnvironmentProjectScope1); - computeEnvironmentScopes1.put(User, computeEnvironmentUserScope1); - - ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions1 = ComputeEnvironmentHardwareOptions.builder() - .allowAllHardware(true) - .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig1, hardwareConfig2))) - .build(); - - computeEnvironmentConfig1 = ComputeEnvironmentConfig.builder() - .configTypes(new HashSet<>(Collections.singletonList(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB))) - .computeEnvironment(computeEnvironment1) - .scopes(computeEnvironmentScopes1) - .hardwareOptions(computeEnvironmentHardwareOptions1) - .build(); - - // Setup second compute environment - ComputeEnvironment computeEnvironment2 = ComputeEnvironment.builder() - .name("XNAT Datascience Notebook") - .image("xnat/datascience-notebook:latest") - .environmentVariables(new ArrayList<>()) - .mounts(new ArrayList<>()) - .build(); - - ComputeEnvironmentScope computeEnvironmentSiteScope2 = ComputeEnvironmentScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeEnvironmentScope computeEnvironmentProjectScope2 = ComputeEnvironmentScope.builder() - .scope(Project) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("Project1"))) - .build(); - - ComputeEnvironmentScope computeEnvironmentUserScope2 = ComputeEnvironmentScope.builder() - .scope(User) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("User1"))) - .build(); - - Map computeEnvironmentScopes2 = new HashMap<>(); - computeEnvironmentScopes2.put(Site, computeEnvironmentSiteScope2); - computeEnvironmentScopes2.put(Project, computeEnvironmentProjectScope2); - computeEnvironmentScopes2.put(User, computeEnvironmentUserScope2); - - ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions2 = ComputeEnvironmentHardwareOptions.builder() - .allowAllHardware(false) - .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig2))) - .build(); - - computeEnvironmentConfig2 = ComputeEnvironmentConfig.builder() - .configTypes(new HashSet<>(Collections.singletonList(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB))) - .computeEnvironment(computeEnvironment2) - .scopes(computeEnvironmentScopes2) - .hardwareOptions(computeEnvironmentHardwareOptions2) - .build(); - - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java deleted file mode 100644 index 8a0b2a8..0000000 --- a/src/test/java/org/nrg/xnat/compute/services/impl/DefaultJobTemplateServiceTest.java +++ /dev/null @@ -1,229 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.models.*; -import org.nrg.xnat.compute.config.DefaultJobTemplateServiceTestConfig; -import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; -import org.nrg.xnat.compute.services.ConstraintConfigService; -import org.nrg.xnat.compute.services.HardwareConfigService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import java.util.*; - -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = DefaultJobTemplateServiceTestConfig.class) -public class DefaultJobTemplateServiceTest { - - @Autowired private DefaultJobTemplateService jobTemplateService; - @Autowired private ComputeEnvironmentConfigService mockComputeEnvironmentConfigService; - @Autowired private HardwareConfigService mockHardwareConfigService; - @Autowired private ConstraintConfigService mockConstraintConfigService; - - @Before - public void before() { - } - - @After - public void after() { - Mockito.reset( - mockComputeEnvironmentConfigService, - mockHardwareConfigService, - mockConstraintConfigService - ); - } - - @Test - public void testIsAvailable_ComputeEnvironmentNotAvailable() { - // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(false); - when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(true); - - Map executionScope = new HashMap<>(); - executionScope.put(Scope.Site, "site"); - executionScope.put(Scope.Project, "project"); - executionScope.put(Scope.User, "user"); - executionScope.put(Scope.DataType, "xnat:petSessionData"); - executionScope.put(Scope.Experiment, "XNAT_E00001"); - - // Run - boolean isAvailable = jobTemplateService.isAvailable(1L, 1L, executionScope); - - // Verify - assertFalse(isAvailable); - } - - @Test - public void testIsAvailable_HardwareNotAvailable() { - // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(true); - when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(false); - - Map executionScope = new HashMap<>(); - executionScope.put(Scope.Project, "project"); - executionScope.put(Scope.User, "user"); - - // Run - boolean isAvailable = jobTemplateService.isAvailable(1L, 1L, executionScope); - - // Verify - assertFalse(isAvailable); - } - - @Test - public void testIsAvailable_ComputeEnvironmentAndHardwareNotAvailable() { - // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(false); - when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(false); - - // Run - Map executionScope = new HashMap<>(); - executionScope.put(Scope.Project, "project"); - executionScope.put(Scope.User, "user"); - boolean isAvailable = jobTemplateService.isAvailable(1L, 1L, executionScope); - - // Verify - assertFalse(isAvailable); - } - - @Test - public void testIsAvailable_ComputeEnvironmentConfigAllHardwareIsAvailable() { - // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(true); - when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(true); - - ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().build(); - ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); - hardwareOptions.setAllowAllHardware(true); - computeEnvironmentConfig.setHardwareOptions(hardwareOptions); - when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); - - Map executionScope = new HashMap<>(); - executionScope.put(Scope.Project, "project"); - executionScope.put(Scope.User, "user"); - - // Run - boolean isAvailable = jobTemplateService.isAvailable(1L, 1L, executionScope); - - // Verify - assertTrue(isAvailable); - } - - @Test - public void testIsAvailable_ComputeEnvironmentConfigSpecificHardwareAllowed() { - // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(true); - when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(true); - - ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().id(1L).build(); - ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); - hardwareOptions.setAllowAllHardware(false); - computeEnvironmentConfig.setHardwareOptions(hardwareOptions); - HardwareConfig hardwareConfig = HardwareConfig.builder().id(1L).build(); - hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); - when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); - - Map executionScope = new HashMap<>(); - executionScope.put(Scope.Project, "project"); - executionScope.put(Scope.User, "user"); - executionScope.put(Scope.DataType, "xnat:petSessionData"); - executionScope.put(Scope.Experiment, "XNAT_E00001"); - - // Run - boolean isAvailable = jobTemplateService.isAvailable(1L, 1L, executionScope); - - // Verify - assertTrue(isAvailable); - } - - @Test - public void testIsAvailable_ComputeEnvironmentConfigSpecificHardwareNotAllowed() { - // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(true); - when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(true); - - ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().id(1L).build(); - ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); - hardwareOptions.setAllowAllHardware(false); - computeEnvironmentConfig.setHardwareOptions(hardwareOptions); - HardwareConfig hardwareConfig = HardwareConfig.builder().id(1L).build(); - hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); - when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); - - Map executionScope = new HashMap<>(); - executionScope.put(Scope.Project, "project"); - executionScope.put(Scope.User, "user"); - executionScope.put(Scope.DataType, "xnat:petSessionData"); - executionScope.put(Scope.Experiment, "XNAT_E00002"); - - // Run - boolean isAvailable = jobTemplateService.isAvailable(1L, 2L, executionScope); - - // Verify - assertFalse(isAvailable); - } - - @Test - public void testResolve() { - // Setup - when(mockComputeEnvironmentConfigService.isAvailable(any(), any())).thenReturn(true); - when(mockHardwareConfigService.isAvailable(any(), any())).thenReturn(true); - - ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().id(1L).build(); - ComputeEnvironment computeEnvironment = ComputeEnvironment.builder() - .name("JupyterHub Data Science Notebook") - .image("jupyter/datascience-notebook:latest") - .build(); - computeEnvironmentConfig.setComputeEnvironment(computeEnvironment); - ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); - hardwareOptions.setAllowAllHardware(false); - computeEnvironmentConfig.setHardwareOptions(hardwareOptions); - HardwareConfig hardwareConfig = HardwareConfig.builder().id(1L).build(); - Hardware hardware = Hardware.builder() - .name("Standard") - .cpuLimit(1.0) - .memoryLimit("4G") - .build(); - hardwareConfig.setHardware(hardware); - hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); - - Constraint constraint = Constraint.builder() - .key("node.role") - .operator(Constraint.Operator.IN) - .values(new HashSet<>(Arrays.asList("worker"))) - .build(); - - ConstraintConfig constraintConfig = ConstraintConfig.builder() - .id(1L) - .constraint(constraint) - .build(); - - when(mockComputeEnvironmentConfigService.retrieve(any())).thenReturn(Optional.of(computeEnvironmentConfig)); - when(mockHardwareConfigService.retrieve(any())).thenReturn(Optional.of(hardwareConfig)); - when(mockConstraintConfigService.getAvailable(any())).thenReturn(Collections.singletonList(constraintConfig)); - - Map executionScope = new HashMap<>(); - executionScope.put(Scope.Project, "project"); - executionScope.put(Scope.User, "user"); - - // Run - JobTemplate jobTemplate = jobTemplateService.resolve(1L, 1L, executionScope); - - // Verify - assertNotNull(jobTemplate); - assertEquals(computeEnvironmentConfig.getComputeEnvironment(), jobTemplate.getComputeEnvironment()); - assertEquals(hardware, jobTemplate.getHardware()); - assertEquals(Collections.singletonList(constraint), jobTemplate.getConstraints()); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityServiceTest.java deleted file mode 100644 index ddd97b6..0000000 --- a/src/test/java/org/nrg/xnat/compute/services/impl/HibernateComputeEnvironmentConfigEntityServiceTest.java +++ /dev/null @@ -1,505 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.nrg.xnat.compute.models.*; -import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.config.HibernateComputeEnvironmentConfigEntityServiceTestConfig; -import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; -import org.nrg.xnat.compute.entities.HardwareConfigEntity; -import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; -import org.nrg.xnat.compute.repositories.HardwareConfigDao; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import javax.transaction.Transactional; -import java.util.*; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.*; -import static org.nrg.framework.constants.Scope.*; -import static org.nrg.xnat.compute.models.ComputeEnvironmentConfig.ConfigType.CONTAINER_SERVICE; -import static org.nrg.xnat.compute.models.ComputeEnvironmentConfig.ConfigType.JUPYTERHUB; -import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; - -@RunWith(SpringJUnit4ClassRunner.class) -@Transactional -@ContextConfiguration(classes = HibernateComputeEnvironmentConfigEntityServiceTestConfig.class) -public class HibernateComputeEnvironmentConfigEntityServiceTest { - - @Autowired private HibernateComputeEnvironmentConfigEntityService hibernateComputeEnvironmentConfigEntityService; - @Autowired @Qualifier("hardwareConfigDaoImpl") private HardwareConfigDao hardwareConfigDaoImpl; - @Autowired @Qualifier("computeEnvironmentConfigDaoImpl") private ComputeEnvironmentConfigDao computeEnvironmentConfigDaoImpl; - - private ComputeEnvironmentConfig computeEnvironmentConfig1; - private ComputeEnvironmentConfig computeEnvironmentConfig2; - private ComputeEnvironmentConfig computeEnvironmentConfig3; - - private HardwareConfig hardwareConfig1; - private HardwareConfig hardwareConfig2; - - @Before - public void before() { - hibernateComputeEnvironmentConfigEntityService.setDao(computeEnvironmentConfigDaoImpl); - createDummyConfigsAndEntities(); - } - - @After - public void after() { - Mockito.reset(); - } - - @Test - public void test() { - assertNotNull(hibernateComputeEnvironmentConfigEntityService); - assertNotNull(hardwareConfigDaoImpl); - assertNotNull(computeEnvironmentConfigDaoImpl); - } - - @Test - @DirtiesContext - public void testCreate() { - // Setup - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity1 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig1); - - // Execute - ComputeEnvironmentConfigEntity created = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity1); - commitTransaction(); - ComputeEnvironmentConfigEntity retrieved = hibernateComputeEnvironmentConfigEntityService.retrieve(created.getId()); - - // Verify - assertNotNull(retrieved); - assertThat(retrieved.toPojo(), is(created.toPojo())); - } - - @Test - @DirtiesContext - public void testAddHardwareConfigEntity() { - // Setup - HardwareConfigEntity hardwareConfigEntity1 = HardwareConfigEntity.fromPojo(hardwareConfig1); - HardwareConfigEntity hardwareConfigEntity2 = HardwareConfigEntity.fromPojo(hardwareConfig2); - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity1 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig1); - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity2 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig2); - - // Commit hardware configs - Long hardwareConfigEntity1_id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity1); - Long hardwareConfigEntity2_Id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity2); - - ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity1); - ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity2); - - commitTransaction(); - - // Verify - assertNotNull(hardwareConfigEntity1_id); - assertNotNull(hardwareConfigEntity2_Id); - assertNotNull(createdComputeEnvironmentConfigEntity1); - assertNotNull(createdComputeEnvironmentConfigEntity2); - - // Execute - // Add hardware configs to compute environment configs - hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity1_id); - hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity2_Id); - hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity2.getId(), hardwareConfigEntity1_id); - - commitTransaction(); - - // Verify - ComputeEnvironmentConfigEntity retrievedComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity1.getId()); - ComputeEnvironmentConfigEntity retrievedComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity2.getId()); - - assertNotNull(retrievedComputeEnvironmentConfigEntity1); - assertNotNull(retrievedComputeEnvironmentConfigEntity2); - assertEquals(2, retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); - assertEquals(1, retrievedComputeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); - assertTrue(retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); - assertTrue(retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity2_Id))); - assertTrue(retrievedComputeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); - } - - @Test - @DirtiesContext - public void testRemoveHardwareConfigEntity() { - // Setup - HardwareConfigEntity hardwareConfigEntity1 = HardwareConfigEntity.fromPojo(hardwareConfig1); - HardwareConfigEntity hardwareConfigEntity2 = HardwareConfigEntity.fromPojo(hardwareConfig2); - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity1 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig1); - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity2 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig2); - - // Commit hardware configs - Long hardwareConfigEntity1_id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity1); - Long hardwareConfigEntity2_Id = (Long) hardwareConfigDaoImpl.create(hardwareConfigEntity2); - - ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity1); - ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity2); - - commitTransaction(); - - // Verify - assertNotNull(hardwareConfigEntity1_id); - assertNotNull(hardwareConfigEntity2_Id); - assertNotNull(createdComputeEnvironmentConfigEntity1); - assertNotNull(createdComputeEnvironmentConfigEntity2); - - // Execute - // Add hardware configs to compute environment configs - hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity1_id); - hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity2_Id); - hibernateComputeEnvironmentConfigEntityService.addHardwareConfigEntity(createdComputeEnvironmentConfigEntity2.getId(), hardwareConfigEntity1_id); - - commitTransaction(); - - // Verify - ComputeEnvironmentConfigEntity retrievedComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity1.getId()); - ComputeEnvironmentConfigEntity retrievedComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity2.getId()); - - assertNotNull(retrievedComputeEnvironmentConfigEntity1); - assertNotNull(retrievedComputeEnvironmentConfigEntity2); - assertEquals(2, retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); - assertEquals(1, retrievedComputeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); - assertTrue(retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); - assertTrue(retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity2_Id))); - assertTrue(retrievedComputeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().stream().map(HardwareConfigEntity::getId).anyMatch(id -> id.equals(hardwareConfigEntity1_id))); - - // Remove hardware configs from compute environment configs - hibernateComputeEnvironmentConfigEntityService.removeHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity1_id); - hibernateComputeEnvironmentConfigEntityService.removeHardwareConfigEntity(createdComputeEnvironmentConfigEntity1.getId(), hardwareConfigEntity2_Id); - - commitTransaction(); - - hibernateComputeEnvironmentConfigEntityService.removeHardwareConfigEntity(createdComputeEnvironmentConfigEntity2.getId(), hardwareConfigEntity1_id); - - commitTransaction(); - - // Verify - retrievedComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity1.getId()); - retrievedComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.retrieve(createdComputeEnvironmentConfigEntity2.getId()); - - assertNotNull(retrievedComputeEnvironmentConfigEntity1); - assertNotNull(retrievedComputeEnvironmentConfigEntity2); - assertEquals(0, retrievedComputeEnvironmentConfigEntity1.getHardwareOptions().getHardwareConfigs().size()); - assertEquals(0, retrievedComputeEnvironmentConfigEntity2.getHardwareOptions().getHardwareConfigs().size()); - } - - @Test - @DirtiesContext - public void testFindByType() { - // Setup - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity1 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig1); - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity2 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig2); - ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity3 = ComputeEnvironmentConfigEntity.fromPojo(computeEnvironmentConfig3); - - // Commit compute environment configs - ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity1 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity1); - ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity2 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity2); - ComputeEnvironmentConfigEntity createdComputeEnvironmentConfigEntity3 = hibernateComputeEnvironmentConfigEntityService.create(computeEnvironmentConfigEntity3); - - commitTransaction(); - - // Verify - assertNotNull(createdComputeEnvironmentConfigEntity1); - assertNotNull(createdComputeEnvironmentConfigEntity2); - assertNotNull(createdComputeEnvironmentConfigEntity3); - - // Execute - List retrievedComputeEnvironmentConfigEntities = hibernateComputeEnvironmentConfigEntityService.findByType(JUPYTERHUB); - - // Verify - assertNotNull(retrievedComputeEnvironmentConfigEntities); - assertEquals(2, retrievedComputeEnvironmentConfigEntities.size()); - assertEquals(createdComputeEnvironmentConfigEntity1.getId(), retrievedComputeEnvironmentConfigEntities.get(0).getId()); - assertEquals(createdComputeEnvironmentConfigEntity3.getId(), retrievedComputeEnvironmentConfigEntities.get(1).getId()); - - // Execute - retrievedComputeEnvironmentConfigEntities = hibernateComputeEnvironmentConfigEntityService.findByType(CONTAINER_SERVICE); - - // Verify - assertNotNull(retrievedComputeEnvironmentConfigEntities); - assertEquals(2, retrievedComputeEnvironmentConfigEntities.size()); - assertEquals(createdComputeEnvironmentConfigEntity2.getId(), retrievedComputeEnvironmentConfigEntities.get(0).getId()); - assertEquals(createdComputeEnvironmentConfigEntity3.getId(), retrievedComputeEnvironmentConfigEntities.get(1).getId()); - } - - public void createDummyConfigsAndEntities() { - // Setup hardware - Hardware hardware1 = Hardware.builder() - .name("Small") - .cpuReservation(2.0) - .cpuLimit(4.0) - .memoryReservation("4G") - .memoryLimit("8G") - .build(); - - // Setup hardware constraints - Constraint hardwareConstraint1 = Constraint.builder() - .key("node.role") - .operator(Constraint.Operator.IN) - .values(new HashSet<>(Collections.singletonList("worker"))) - .build(); - - Constraint hardwareConstraint2 = Constraint.builder() - .key("node.instance.type") - .operator(Constraint.Operator.NOT_IN) - .values(new HashSet<>(Arrays.asList("spot", "demand"))) - .build(); - - hardware1.setConstraints(Arrays.asList(hardwareConstraint1, hardwareConstraint2)); - - // Setup hardware environment variables - EnvironmentVariable hardwareEnvironmentVariable1 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); - EnvironmentVariable hardwareEnvironmentVariable2 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); - - hardware1.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable1, hardwareEnvironmentVariable2)); - - // Setup hardware generic resources - GenericResource hardwareGenericResource1 = new GenericResource("nvidia.com/gpu", "2"); - GenericResource hardwareGenericResource2 = new GenericResource("fpga.com/fpga", "1"); - - hardware1.setGenericResources(Arrays.asList(hardwareGenericResource1, hardwareGenericResource2)); - - // Setup hardware scopes - HardwareScope hardwareSiteScope1 = HardwareScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope hardwareProjectScope1 = HardwareScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope userHardwareScope1 = HardwareScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map hardwareScopes1 = new HashMap<>(); - hardwareScopes1.put(Site, hardwareSiteScope1); - hardwareScopes1.put(Project, hardwareProjectScope1); - hardwareScopes1.put(User, userHardwareScope1); - - // Setup a hardware config entity - hardwareConfig1 = HardwareConfig.builder() - .hardware(hardware1) - .scopes(hardwareScopes1) - .build(); - - // Setup second hardware config - Hardware hardware2 = Hardware.builder() - .name("Medium") - .cpuReservation(4.0) - .cpuLimit(4.0) - .memoryReservation("8G") - .memoryLimit("8G") - .build(); - - // Setup hardware constraints - Constraint hardwareConstraint3 = Constraint.builder() - .key("node.role") - .operator(Constraint.Operator.IN) - .values(new HashSet<>(Collections.singletonList("worker"))) - .build(); - - Constraint hardwareConstraint4 = Constraint.builder() - .key("node.instance.type") - .operator(Constraint.Operator.NOT_IN) - .values(new HashSet<>(Arrays.asList("spot", "demand"))) - .build(); - - hardware2.setConstraints(Arrays.asList(hardwareConstraint3, hardwareConstraint4)); - - // Setup hardware environment variables - EnvironmentVariable hardwareEnvironmentVariable3 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); - EnvironmentVariable hardwareEnvironmentVariable4 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); - - hardware2.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable3, hardwareEnvironmentVariable4)); - - // Setup hardware generic resources - GenericResource hardwareGenericResource3 = new GenericResource("nvidia.com/gpu", "2"); - GenericResource hardwareGenericResource4 = new GenericResource("fpga.com/fpga", "1"); - - hardware2.setGenericResources(Arrays.asList(hardwareGenericResource3, hardwareGenericResource4)); - - // Setup hardware scopes - HardwareScope hardwareSiteScope2 = HardwareScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope hardwareProjectScope2 = HardwareScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - HardwareScope userHardwareScope2 = HardwareScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map hardwareScopes2 = new HashMap<>(); - hardwareScopes2.put(Site, hardwareSiteScope2); - hardwareScopes2.put(Project, hardwareProjectScope2); - hardwareScopes2.put(User, userHardwareScope2); - - // Setup second hardware config entity - hardwareConfig2 = HardwareConfig.builder() - .hardware(hardware2) - .scopes(hardwareScopes2) - .build(); - - // Setup first compute environment - // Create mount first - Mount mount1 = Mount.builder() - .localPath("/home/jovyan/work") - .containerPath("/home/jovyan/work") - .readOnly(false) - .build(); - - Mount mount2 = Mount.builder() - .localPath("/tools/MATLAB/R2019b") - .containerPath("/tools/MATLAB/R2019b") - .readOnly(true) - .build(); - - ComputeEnvironment computeEnvironment1 = ComputeEnvironment.builder() - .name("Jupyter Datascience Notebook") - .image("jupyter/datascience-notebook:hub-3.0.0") - .environmentVariables(Collections.singletonList(new EnvironmentVariable("JUPYTER_ENABLE_LAB", "yes"))) - .mounts(Arrays.asList(mount1, mount2)) - .build(); - - ComputeEnvironmentScope computeEnvironmentSiteScope1 = ComputeEnvironmentScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeEnvironmentScope computeEnvironmentProjectScope1 = ComputeEnvironmentScope.builder() - .scope(Project) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeEnvironmentScope computeEnvironmentUserScope1 = ComputeEnvironmentScope.builder() - .scope(User) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - Map computeEnvironmentScopes1 = new HashMap<>(); - computeEnvironmentScopes1.put(Site, computeEnvironmentSiteScope1); - computeEnvironmentScopes1.put(Project, computeEnvironmentProjectScope1); - computeEnvironmentScopes1.put(User, computeEnvironmentUserScope1); - - ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions1 = ComputeEnvironmentHardwareOptions.builder() - .allowAllHardware(true) - .hardwareConfigs(new HashSet<>()) - .build(); - - computeEnvironmentConfig1 = ComputeEnvironmentConfig.builder() - .configTypes(new HashSet<>(Collections.singletonList(JUPYTERHUB))) - .computeEnvironment(computeEnvironment1) - .scopes(computeEnvironmentScopes1) - .hardwareOptions(computeEnvironmentHardwareOptions1) - .build(); - - // Setup second compute environment - ComputeEnvironment computeEnvironment2 = ComputeEnvironment.builder() - .name("XNAT Datascience Notebook") - .image("xnat/datascience-notebook:latest") - .environmentVariables(new ArrayList<>()) - .mounts(new ArrayList<>()) - .build(); - - ComputeEnvironmentScope computeEnvironmentSiteScope2 = ComputeEnvironmentScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeEnvironmentScope computeEnvironmentProjectScope2 = ComputeEnvironmentScope.builder() - .scope(Project) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("Project1"))) - .build(); - - ComputeEnvironmentScope computeEnvironmentUserScope2 = ComputeEnvironmentScope.builder() - .scope(User) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("User1"))) - .build(); - - Map computeEnvironmentScopes2 = new HashMap<>(); - computeEnvironmentScopes2.put(Site, computeEnvironmentSiteScope2); - computeEnvironmentScopes2.put(Project, computeEnvironmentProjectScope2); - computeEnvironmentScopes2.put(User, computeEnvironmentUserScope2); - - ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions2 = ComputeEnvironmentHardwareOptions.builder() - .allowAllHardware(false) - .hardwareConfigs(new HashSet<>()) - .build(); - - computeEnvironmentConfig2 = ComputeEnvironmentConfig.builder() - .configTypes(new HashSet<>(Collections.singletonList(CONTAINER_SERVICE))) - .computeEnvironment(computeEnvironment2) - .scopes(computeEnvironmentScopes2) - .hardwareOptions(computeEnvironmentHardwareOptions2) - .build(); - - // Setup third compute environment - ComputeEnvironment computeEnvironment3 = ComputeEnvironment.builder() - .name("MATLAB Datascience Notebook") - .image("matlab/datascience-notebook:latest") - .environmentVariables(new ArrayList<>()) - .mounts(new ArrayList<>()) - .build(); - - ComputeEnvironmentScope computeEnvironmentSiteScope3 = ComputeEnvironmentScope.builder() - .scope(Site) - .enabled(true) - .ids(new HashSet<>(Collections.emptyList())) - .build(); - - ComputeEnvironmentScope computeEnvironmentProjectScope3 = ComputeEnvironmentScope.builder() - .scope(Project) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("Project1"))) - .build(); - - ComputeEnvironmentScope computeEnvironmentUserScope3 = ComputeEnvironmentScope.builder() - .scope(User) - .enabled(false) - .ids(new HashSet<>(Collections.singletonList("User1"))) - .build(); - - Map computeEnvironmentScopes3 = new HashMap<>(); - computeEnvironmentScopes2.put(Site, computeEnvironmentSiteScope3); - computeEnvironmentScopes2.put(Project, computeEnvironmentProjectScope3); - computeEnvironmentScopes2.put(User, computeEnvironmentUserScope3); - - ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions3 = ComputeEnvironmentHardwareOptions.builder() - .allowAllHardware(false) - .hardwareConfigs(new HashSet<>()) - .build(); - - computeEnvironmentConfig3 = ComputeEnvironmentConfig.builder() - .configTypes(new HashSet<>(Arrays.asList(CONTAINER_SERVICE, JUPYTERHUB))) - .computeEnvironment(computeEnvironment3) - .scopes(computeEnvironmentScopes3) - .hardwareOptions(computeEnvironmentHardwareOptions3) - .build(); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityServiceTest.java deleted file mode 100644 index 6c6e872..0000000 --- a/src/test/java/org/nrg/xnat/compute/services/impl/HibernateConstraintConfigEntityServiceTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.nrg.framework.constants.Scope; -import org.nrg.xnat.compute.config.HibernateConstraintConfigEntityServiceTestConfig; -import org.nrg.xnat.compute.entities.ConstraintConfigEntity; -import org.nrg.xnat.compute.models.Constraint; -import org.nrg.xnat.compute.models.ConstraintConfig; -import org.nrg.xnat.compute.models.ConstraintScope; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.nrg.xnat.compute.utils.TestingUtils.commitTransaction; - -@RunWith(SpringJUnit4ClassRunner.class) -@Transactional -@ContextConfiguration(classes = HibernateConstraintConfigEntityServiceTestConfig.class) -public class HibernateConstraintConfigEntityServiceTest { - - @Autowired private HibernateConstraintConfigEntityService hibernateConstraintConfigEntityService; - - @Test - public void test() { - assertNotNull(hibernateConstraintConfigEntityService); - } - - @Test - @DirtiesContext - public void testCreateConstraintConfig() { - assertNotNull(hibernateConstraintConfigEntityService); - - // Create a constraint config - Constraint constraint1 = Constraint.builder() - .key("node.role") - .operator(Constraint.Operator.IN) - .values(new HashSet<>(Arrays.asList("worker"))) - .build(); - - ConstraintScope constraintScopeSite1 = ConstraintScope.builder() - .scope(Scope.Site) - .enabled(true) - .ids(new HashSet<>()) - .build(); - - ConstraintScope constraintScopeProject1 = ConstraintScope.builder() - .scope(Scope.Project) - .enabled(true) - .ids(new HashSet<>()) - .build(); - - ConstraintScope constraintScopeUser1 = ConstraintScope.builder() - .scope(Scope.User) - .enabled(true) - .ids(new HashSet<>()) - .build(); - - Map scopes1 = new HashMap<>(); - scopes1.put(Scope.Site, constraintScopeSite1); - scopes1.put(Scope.Project, constraintScopeProject1); - scopes1.put(Scope.User, constraintScopeUser1); - - ConstraintConfig constraintConfig = ConstraintConfig.builder() - .constraint(constraint1) - .scopes(scopes1) - .build(); - - // Save the constraint config - ConstraintConfigEntity created = hibernateConstraintConfigEntityService.create(ConstraintConfigEntity.fromPojo(constraintConfig)); - - commitTransaction(); - - // Retrieve the constraint config - ConstraintConfigEntity retrieved = hibernateConstraintConfigEntityService.retrieve(created.getId()); - - // Check that the retrieved constraint config matches the original - assertEquals(created, retrieved); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityServiceTest.java b/src/test/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityServiceTest.java deleted file mode 100644 index 8902fd2..0000000 --- a/src/test/java/org/nrg/xnat/compute/services/impl/HibernateHardwareConfigEntityServiceTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.nrg.xnat.compute.services.impl; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.nrg.xnat.compute.config.HibernateHardwareConfigEntityServiceTestConfig; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import static org.junit.Assert.*; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = HibernateHardwareConfigEntityServiceTestConfig.class) -public class HibernateHardwareConfigEntityServiceTest { - - @Autowired private HibernateHardwareConfigEntityService hibernateHardwareConfigEntityService; - - @Test - public void test() { - assertNotNull(hibernateHardwareConfigEntityService); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnat/compute/utils/TestingUtils.java b/src/test/java/org/nrg/xnat/compute/utils/TestingUtils.java deleted file mode 100644 index 76a2129..0000000 --- a/src/test/java/org/nrg/xnat/compute/utils/TestingUtils.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.nrg.xnat.compute.utils; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.test.context.transaction.TestTransaction; - -@Slf4j -public class TestingUtils { - - public static void commitTransaction() { - TestTransaction.flagForCommit(); - TestTransaction.end(); - TestTransaction.start(); - } - -} From f92f73175725b5202548cb8a609371207e2b5b8e Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Fri, 25 Aug 2023 16:01:31 -0500 Subject: [PATCH 32/49] JHP-66 and XNAT-7867: Maven local shouldn't have been first repository --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5bbc4d1..4abc044 100644 --- a/build.gradle +++ b/build.gradle @@ -14,9 +14,9 @@ version "1.1.0-SNAPSHOT" description "JupyterHub Plugin for XNAT" repositories { - mavenLocal() maven { url "https://nrgxnat.jfrog.io/nrgxnat/libs-release" } maven { url "https://nrgxnat.jfrog.io/nrgxnat/libs-snapshot" } + mavenLocal() mavenCentral() maven { url "https://www.dcm4che.org/maven2" } } From 16e78e8ed4722406a819fe6ae7a9e7602011780c Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Wed, 20 Sep 2023 13:19:16 -0500 Subject: [PATCH 33/49] JHP-68: Update date format patterns to handle optional fractions of seconds --- .../nrg/xnatx/plugins/jupyterhub/client/models/Server.java | 4 ++-- .../nrg/xnatx/plugins/jupyterhub/client/models/Token.java | 6 +++--- .../nrg/xnatx/plugins/jupyterhub/client/models/User.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/Server.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/Server.java index 0e7ce8f..542bc4a 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/Server.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/Server.java @@ -23,6 +23,6 @@ public class Server { private String url; private String progress_url; private Map user_options; - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSX", timezone = "UTC") private ZonedDateTime started; - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSX", timezone = "UTC") private ZonedDateTime last_activity; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X", timezone = "UTC") private ZonedDateTime started; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X", timezone = "UTC") private ZonedDateTime last_activity; } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/Token.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/Token.java index 1d55a23..f601e79 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/Token.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/Token.java @@ -27,10 +27,10 @@ public class Token { private String oauth_client; private String session_id; - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSX", timezone = "UTC") private ZonedDateTime expires_at; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X", timezone = "UTC") private ZonedDateTime expires_at; private int expires_in; // lifetime (in seconds) after which the requested token will expire - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSX", timezone = "UTC") private ZonedDateTime created; - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSX", timezone = "UTC") private ZonedDateTime last_activity; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X", timezone = "UTC") private ZonedDateTime created; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X", timezone = "UTC") private ZonedDateTime last_activity; } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/User.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/User.java index 9a71968..da7a57d 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/User.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/client/models/User.java @@ -25,7 +25,7 @@ public class User { private String server; // Default, unnamed server private String pending; - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSX", timezone = "UTC") + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X", timezone = "UTC") private ZonedDateTime last_activity; // Servername -> Server From d512947f101ba999b7e8485ab238fbd626307e3f Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 2 Oct 2023 17:29:11 -0400 Subject: [PATCH 34/49] JHP-69: Adds a new preference for the JupyterHub Host URL --- .../JupyterHubPreferenceInitializer.java | 82 +++++++++ .../preferences/JupyterHubPreferences.java | 14 ++ .../rest/JupyterHubPreferencesApi.java | 4 + .../jupyterhub/utils/SystemHelper.java | 9 + .../utils/impl/DefaultSystemHelper.java | 11 ++ .../plugin/jupyterhub/jupyterhub-servers.js | 13 +- .../spawner/jupyterhub/site-settings.yaml | 51 +++++- .../config/DefaultSystemHelperTestConfig.java | 17 ++ ...terHubPreferenceInitializerTestConfig.java | 31 ++++ .../plugins/jupyterhub/config/MockConfig.java | 6 + .../JupyterHubPreferenceInitializerTest.java | 155 ++++++++++++++++++ .../utils/impl/DefaultSystemHelperTest.java | 28 ++++ 12 files changed, 413 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializer.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/SystemHelper.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelper.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultSystemHelperTestConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubPreferenceInitializerTestConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializerTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelperTest.java diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializer.java new file mode 100644 index 0000000..4f0fd4b --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializer.java @@ -0,0 +1,82 @@ +package org.nrg.xnatx.plugins.jupyterhub.initialize; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.xdat.preferences.SiteConfigPreferences; +import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; +import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.utils.SystemHelper; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * This class is used to initialize JupyterHub plugin preferences. + */ +@Component +@Slf4j +public class JupyterHubPreferenceInitializer extends AbstractInitializingTask { + + private final XFTManagerHelper xftManagerHelper; + private final XnatAppInfo appInfo; + private final JupyterHubPreferences jupyterHubPreferences; + private final SiteConfigPreferences siteConfigPreferences; + private final SystemHelper systemHelper; + + @Autowired + public JupyterHubPreferenceInitializer(final XFTManagerHelper xftManagerHelper, + final XnatAppInfo appInfo, + final JupyterHubPreferences jupyterHubPreferences, + final SiteConfigPreferences siteConfigPreferences, + final SystemHelper systemHelper) { + this.xftManagerHelper = xftManagerHelper; + this.appInfo = appInfo; + this.jupyterHubPreferences = jupyterHubPreferences; + this.siteConfigPreferences = siteConfigPreferences; + this.systemHelper = systemHelper; + } + + @Override + public String getTaskName() { + return "JupyterHubPreferenceInitializer"; + } + + @Override + protected void callImpl() throws InitializingTaskException { + log.info("Initializing JupyterHub preferences."); + + if (!xftManagerHelper.isInitialized()) { + log.info("XFT not initialized, deferring execution."); + throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); + } + + if (!appInfo.isInitialized()) { + log.info("XNAT not initialized, deferring execution."); + throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); + } + + // Initialize the JupyterHub preferences. + initializeJupyterHubHostUrl(); + } + + /** + * Initialize the JupyterHub Host URL preference. + * If the JupyterHub Host URL preference is not set, check for the JH_HOST_URL environment variable. If the + * environment variable is not set, default to the XNAT site url. + */ + protected void initializeJupyterHubHostUrl() { + // Check if the JupyterHub host url preference is set. + if (jupyterHubPreferences.getJupyterHubHostUrl() == null || jupyterHubPreferences.getJupyterHubHostUrl().isEmpty()) { + // Check for env variable or default to site url. + String jupyterHubHostUrl = systemHelper.getEnv("JH_HOST_URL"); + if (jupyterHubHostUrl == null || jupyterHubHostUrl.isEmpty()) { + jupyterHubHostUrl = siteConfigPreferences.getSiteUrl(); + } + jupyterHubPreferences.setJupyterHubHostUrl(jupyterHubHostUrl); + log.info("JupyterHub host url initialized to: " + jupyterHubHostUrl); + } else { + log.info("JupyterHub host url already set. Skipping initialization."); + } + } +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java index 0f4f417..752bbde 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java @@ -22,6 +22,7 @@ public class JupyterHubPreferences extends AbstractPreferenceBean { public static final String TOOL_ID = "jupyterhub"; + public static final String JUPYTERHUB_HOST_URL = "jupyterHubHostUrl"; public static final String ALL_USERS_JUPYTER = "allUsersCanStartJupyter"; public static final String DOCKER_IMAGES_PREF_ID = "dockerImages"; public static final String CONTAINER_SPEC_LABELS_PREF_ID = "containerSpecLabels"; @@ -38,6 +39,19 @@ protected JupyterHubPreferences(NrgPreferenceService preferenceService, ConfigPa super(preferenceService, configFolderPaths, initPrefs); } + @NrgPreference(defaultValue = "") + public String getJupyterHubHostUrl() { + return getValue(JUPYTERHUB_HOST_URL); + } + + public void setJupyterHubHostUrl(final String jupyterHubHostUrl) { + try { + set(jupyterHubHostUrl, JUPYTERHUB_HOST_URL); + } catch (InvalidPreferenceName e) { + log.error("Invalid preference name " + JUPYTERHUB_HOST_URL + ": something is very wrong here.", e); + } + } + @NrgPreference(defaultValue = "http://172.17.0.1/jupyterhub/hub/api") public String getJupyterHubApiUrl() { return getValue("jupyterHubApiUrl"); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java index 29cf1a0..16802a8 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubPreferencesApi.java @@ -123,6 +123,10 @@ public Map getSpecifiedPreference(@ApiParam(value = "The Jupyter value = jupyterHubPreferences.getAllUsersCanStartJupyter(); break; } + case (JupyterHubPreferences.JUPYTERHUB_HOST_URL): { + value = jupyterHubPreferences.getJupyterHubHostUrl(); + break; + } default: value = jupyterHubPreferences.get(preference); } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/SystemHelper.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/SystemHelper.java new file mode 100644 index 0000000..81f2a2f --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/SystemHelper.java @@ -0,0 +1,9 @@ +package org.nrg.xnatx.plugins.jupyterhub.utils; + +public interface SystemHelper { + + default String getEnv(String name) { + return System.getenv(name); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelper.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelper.java new file mode 100644 index 0000000..cd65106 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelper.java @@ -0,0 +1,11 @@ +package org.nrg.xnatx.plugins.jupyterhub.utils.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.xnatx.plugins.jupyterhub.utils.SystemHelper; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class DefaultSystemHelper implements SystemHelper { + +} diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js index f0264a6..5c5befd 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js @@ -445,11 +445,18 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s } XNAT.plugin.jupyterhub.servers.goTo = function(server_url) { - XNAT.plugin.jupyterhub.users.tokens.create().then(token => { - window.open(`${server_url}?token=${token['token']}`, '_blank'); + Promise.all([ + XNAT.plugin.jupyterhub.preferences.get('jupyterHubHostUrl'), + XNAT.plugin.jupyterhub.users.tokens.create() + ]).then(([jupyterHubHostUrl, token]) => { + // Remove trailing slash from jupyterHubHostUrl (if present) + jupyterHubHostUrl = jupyterHubHostUrl.replace(/\/$/, ""); + + // open new tab to Jupyter notebook server + window.open(`${jupyterHubHostUrl}${server_url}?token=${token['token']}`, '_blank'); }).catch(() => { window.open(server_url, '_blank'); - }) + }); } XNAT.plugin.jupyterhub.servers.stopServer = async function(username = window.username, diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index dd7173a..ac084a0 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -7,13 +7,53 @@ workspacePath: The directory (as it appears to XNAT) containing user namespaced subdirectories (workspaces). JupyterHub is responsible for mounting a user's workspace directory to the container running their Jupyter notebook server. +jupyterHubHostUrl: + kind: panel.element + label: JupyterHub Host URL + info: > +

    The JupyterHub Host URL is used by XNAT to construct links to a user's Jupyter notebook server. + This is typically the same as your XNAT site URL but could be different depending on your XNAT deployment, + how you have configured JupyterHub, and how you have configured your reverse proxy.

    +

    Include the protocol (http or https), hostname, and port number (if necessary).

    +

    Exclude any trailing /jupyterhub path. JupyterHub will provide the path to the user's notebook server.

    +

    Examples:

    +
      +
    • http://localhost
    • +
    • http://localhost:8081
    • +
    • https://xnat.myorg.com
    • +
    + contents: + jupyterHubHostUrlInput: + kind: input.text + name: jupyterHubHostUrl + after: > +

    + The JupyterHub Host URL is used by XNAT to construct links to a user's Jupyter notebook server. Typically + this is the same as your XNAT site URL. +

    + jupyterhubApiUrl: - kind: panel.input.text - id: jupyterHubApiUrl - name: jupyterHubApiUrl + kind: panel.element label: JupyterHub API URL - description: > - The URL for the JupyterHub API + info: > +

    The JupyterHub API URL is used by XNAT to communicate with JupyterHub.

    +

    Include the protocol (http or https), hostname, and port number (if necessary).

    +

    Also include the path to the JupyterHub API, i.e. /jupyterhub/hub/api.

    +

    Examples:

    +
      +
    • http://172.17.0.1/jupyterhub/hub/api
    • +
    • http://host.docker.internal/jupyterhub/hub/api
    • +
    • http://proxy-public/jupyterhub/hub/api
    • +
    • https://xnat.myorg.com/jupyterhub/hub/api
    • +
    + contents: + jupyterHubApiUrlInput: + kind: input.text + name: jupyterHubApiUrl + after: > +

    + The URL for the JupyterHub API. Used by XNAT to communicate with JupyterHub. +

    jupyterHubToken: kind: panel.input.password @@ -239,6 +279,7 @@ jupyterhubPreferences: contentType: json url: /xapi/jupyterhub/preferences contents: + ${jupyterHubHostUrl} ${jupyterhubApiUrl} ${jupyterHubToken} ${startTimeout} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultSystemHelperTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultSystemHelperTestConfig.java new file mode 100644 index 0000000..b807c45 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultSystemHelperTestConfig.java @@ -0,0 +1,17 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.xnatx.plugins.jupyterhub.utils.impl.DefaultSystemHelper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({MockConfig.class}) +public class DefaultSystemHelperTestConfig { + + @Bean + public DefaultSystemHelper defaultSystemHelper() { + return new DefaultSystemHelper(); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubPreferenceInitializerTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubPreferenceInitializerTestConfig.java new file mode 100644 index 0000000..1c949f5 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubPreferenceInitializerTestConfig.java @@ -0,0 +1,31 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.xdat.preferences.SiteConfigPreferences; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.initialize.JupyterHubPreferenceInitializer; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.utils.SystemHelper; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({MockConfig.class}) +public class JupyterHubPreferenceInitializerTestConfig { + + @Bean + public JupyterHubPreferenceInitializer JupyterHubPreferenceInitializer(final XFTManagerHelper mockXFTManagerHelper, + final XnatAppInfo mockXnatAppInfo, + final JupyterHubPreferences mockJupyterHubPreferences, + final SiteConfigPreferences mockSiteConfigPreferences, + final SystemHelper mockSystemHelper) { + return new JupyterHubPreferenceInitializer( + mockXFTManagerHelper, + mockXnatAppInfo, + mockJupyterHubPreferences, + mockSiteConfigPreferences, + mockSystemHelper + ); + } +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java index 7344f77..898068b 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java @@ -19,6 +19,7 @@ import org.nrg.xnatx.plugins.jupyterhub.services.*; import org.nrg.xnatx.plugins.jupyterhub.utils.JupyterHubServiceAccountHelper; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; +import org.nrg.xnatx.plugins.jupyterhub.utils.SystemHelper; import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; @@ -93,6 +94,11 @@ public PermissionsHelper mockPermissionsHelper() { return Mockito.mock(PermissionsHelper.class); } + @Bean + public SystemHelper mockSystemHelper() { + return Mockito.mock(SystemHelper.class); + } + @Bean public UserWorkspaceService mockUserWorkspaceService() { return Mockito.mock(UserWorkspaceService.class); diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializerTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializerTest.java new file mode 100644 index 0000000..b7e243b --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializerTest.java @@ -0,0 +1,155 @@ +package org.nrg.xnatx.plugins.jupyterhub.initialize; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.xdat.preferences.SiteConfigPreferences; +import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubPreferenceInitializerTestConfig; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.utils.SystemHelper; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.hamcrest.CoreMatchers.isA; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = JupyterHubPreferenceInitializerTestConfig.class) +public class JupyterHubPreferenceInitializerTest { + + @Autowired private JupyterHubPreferenceInitializer jupyterHubPreferenceInitializer; + @Autowired private XFTManagerHelper mockXFTManagerHelper; + @Autowired private XnatAppInfo mockXnatAppInfo; + @Autowired private JupyterHubPreferences mockJupyterHubPreferences; + @Autowired private SiteConfigPreferences mockSiteConfigPreferences; + @Autowired private SystemHelper mockSystemHelper; + + @Before + public void setUp() { + assertNotNull(jupyterHubPreferenceInitializer); + assertNotNull(mockXFTManagerHelper); + assertNotNull(mockXnatAppInfo); + assertNotNull(mockJupyterHubPreferences); + assertNotNull(mockSystemHelper); + } + + @After + public void tearDown() { + Mockito.reset( + mockXFTManagerHelper, + mockXnatAppInfo, + mockJupyterHubPreferences, + mockSystemHelper + ); + } + + @Test + public void getTaskName() { + // Test + String taskName = jupyterHubPreferenceInitializer.getTaskName(); + + // Verify + assertThat(taskName, notNullValue()); + assertThat(taskName, isA(String.class)); + } + + @Test(expected = InitializingTaskException.class) + public void callImpl_notInitialized1() throws Exception { + // Setup + when(mockXFTManagerHelper.isInitialized()).thenReturn(false); + when(mockXnatAppInfo.isInitialized()).thenReturn(false); + + // Test + jupyterHubPreferenceInitializer.callImpl(); + + // Verify + // Exception thrown + verify(mockJupyterHubPreferences, never()).setJupyterHubHostUrl(any()); + } + + @Test(expected = InitializingTaskException.class) + public void callImpl_notInitialized2() throws Exception { + // Setup + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(false); + + // Test + jupyterHubPreferenceInitializer.callImpl(); + + // Verify + // Exception thrown + verify(mockJupyterHubPreferences, never()).setJupyterHubHostUrl(any()); + } + + @Test + public void callImpl_initialized() throws Exception { + // Setup + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + + // Test + jupyterHubPreferenceInitializer.callImpl(); + + // Verify + verify(mockJupyterHubPreferences, times(1)).setJupyterHubHostUrl(any()); + } + + @Test + public void callImpl_initialized_alreadySet() throws Exception { + // Setup + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + when(mockJupyterHubPreferences.getJupyterHubHostUrl()).thenReturn("https://my-xnat.com"); + + // Test + jupyterHubPreferenceInitializer.callImpl(); + + // Verify + verify(mockJupyterHubPreferences, never()).setJupyterHubHostUrl(any()); + } + + + @Test + public void callImpl_initialized_envSet() throws Exception { + // Setup + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + when(mockJupyterHubPreferences.getJupyterHubHostUrl()).thenReturn(""); + + when(mockSystemHelper.getEnv("JH_HOST_URL")).thenReturn("https://my-xnat.com"); + when(mockSiteConfigPreferences.getSiteUrl()).thenReturn("https://another-xnat.com"); + + // Test + jupyterHubPreferenceInitializer.callImpl(); + + // Verify + verify(mockJupyterHubPreferences).setJupyterHubHostUrl("https://my-xnat.com"); + } + + @Test + public void callImpl_initialized_envNotSet() throws Exception { + // Setup + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + when(mockJupyterHubPreferences.getJupyterHubHostUrl()).thenReturn(""); + + when(mockSystemHelper.getEnv("JH_HOST_URL")).thenReturn(""); + when(mockSiteConfigPreferences.getSiteUrl()).thenReturn("https://another-xnat.com"); + + // Test + jupyterHubPreferenceInitializer.callImpl(); + + // Verify + verify(mockJupyterHubPreferences).setJupyterHubHostUrl("https://another-xnat.com"); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelperTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelperTest.java new file mode 100644 index 0000000..14b945a --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelperTest.java @@ -0,0 +1,28 @@ +package org.nrg.xnatx.plugins.jupyterhub.utils.impl; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.nrg.xnatx.plugins.jupyterhub.config.DefaultSystemHelperTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = DefaultSystemHelperTestConfig.class) +public class DefaultSystemHelperTest { + + @Autowired private DefaultSystemHelper systemHelper; + + @Test + public void testNotNull() { + assertNotNull(systemHelper); + } + + @Test + public void test() { + systemHelper.getEnv("PATH"); + } + +} \ No newline at end of file From c945b4ce423389a0e1c169b6b90550d7a937ab0f Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Thu, 12 Oct 2023 11:00:56 -0400 Subject: [PATCH 35/49] Add a changelog. --- CHANGELOG.MD | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 CHANGELOG.MD diff --git a/CHANGELOG.MD b/CHANGELOG.MD new file mode 100644 index 0000000..38c3424 --- /dev/null +++ b/CHANGELOG.MD @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.1] - 2023-10-12 + +### Features +- Added changelog. + +### Fixed +- [JHP-68][]: Updates date format patterns in the Server, Token, and User class to handle optional fractions of seconds. +- [JHP-69][]: Adds a new preference for JupyterHub Host URL. This preference is used to build the URL for linking users + to their Jupyter notebook server. Addresses an issue encountered in multi-node XNAT deployments. + The new preference will default to the site URL (which will keep the current behavior). + +[JHP-68]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-68 +[JHP-69]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-69 From efdba4870b4fdbd3dc994f91b44851f5377002ae Mon Sep 17 00:00:00 2001 From: Ian Gauthier Date: Mon, 16 Oct 2023 15:41:41 +0000 Subject: [PATCH 36/49] XNAT-7903 Jupyter notebooks run on project data should now have access to both native and shared data for that project. Approved-by: John Flavin Approved-by: Andy Lassiter --- build.gradle | 2 +- .../plugins/jupyterhub/JupyterHubPlugin.java | 5 ++ .../preferences/JupyterHubPreferences.java | 2 + .../services/JupyterHubService.java | 2 + .../services/UserOptionsService.java | 1 + .../impl/DefaultJupyterHubService.java | 38 +++++++++++- .../impl/DefaultUserOptionsService.java | 60 ++++++++++++++----- 7 files changed, 92 insertions(+), 18 deletions(-) diff --git a/build.gradle b/build.gradle index 4abc044..5bbc4d1 100644 --- a/build.gradle +++ b/build.gradle @@ -14,9 +14,9 @@ version "1.1.0-SNAPSHOT" description "JupyterHub Plugin for XNAT" repositories { + mavenLocal() maven { url "https://nrgxnat.jfrog.io/nrgxnat/libs-release" } maven { url "https://nrgxnat.jfrog.io/nrgxnat/libs-snapshot" } - mavenLocal() mavenCentral() maven { url "https://www.dcm4che.org/maven2" } } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java index 1510220..93c7adf 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java @@ -63,4 +63,9 @@ public TriggerTask cullLongRunningServers(final JupyterHubService jupyterHubServ return new TriggerTask(jupyterHubService::cullLongRunningServers, new PeriodicTrigger(5, TimeUnit.MINUTES)); } + @Bean + public TriggerTask cullDeadServerSharedFolders(final JupyterHubService jupyterHubService) { + return new TriggerTask(jupyterHubService::cleanupOrphanedSharedDataDirs, new PeriodicTrigger(1, TimeUnit.DAYS)); + } + } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java index 752bbde..affab88 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java @@ -33,6 +33,8 @@ public class JupyterHubPreferences extends AbstractPreferenceBean { public static final String RESOURCE_SPEC_MEM_RESERVATION_PREF_ID = "resourceSpecMemReservation"; public static final String INACTIVITY_TIMEOUT_PREF_ID = "inactivityTimeout"; public static final String MAX_SERVER_LIFETIME_PREF_ID = "maxServerLifetime"; + public static final String SHARED_PROJECT_STRING = "jupyter-notebooks"; + @Autowired protected JupyterHubPreferences(NrgPreferenceService preferenceService, ConfigPaths configFolderPaths, OrderedProperties initPrefs) { diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java index 795618e..2bef514 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/JupyterHubService.java @@ -7,6 +7,7 @@ import org.nrg.xnatx.plugins.jupyterhub.client.models.User; import org.nrg.xnatx.plugins.jupyterhub.models.ServerStartRequest; +import java.io.IOException; import java.util.List; import java.util.Optional; @@ -25,5 +26,6 @@ public interface JupyterHubService { Token createToken(UserI user, String note, Integer expiresIn); void cullInactiveServers(); void cullLongRunningServers(); + void cleanupOrphanedSharedDataDirs(); } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java index 4780f04..eedca21 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java @@ -10,6 +10,7 @@ public interface UserOptionsService { Map getProjectPaths(UserI user, List projectIds); + Map getProjectPaths(UserI user, List projectIds, String eventTrackingId); Map getSubjectPaths(UserI user, String subjectId); Map getSubjectPaths(UserI user, List subjectIds); Map getExperimentPath(UserI user, String experimentId); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java index c48bce3..58c7f71 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java @@ -1,13 +1,16 @@ package org.nrg.xnatx.plugins.jupyterhub.services.impl; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.nrg.framework.constants.Scope; import org.nrg.framework.services.NrgEventServiceI; +import org.nrg.xdat.XDAT; import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xdat.security.user.exceptions.UserInitException; import org.nrg.xft.security.UserI; +import org.nrg.xnat.utils.FileUtils; import org.nrg.xnatx.plugins.jupyterhub.client.JupyterHubClient; import org.nrg.xnatx.plugins.jupyterhub.client.exceptions.ResourceAlreadyExistsException; import org.nrg.xnatx.plugins.jupyterhub.client.exceptions.UserNotFoundException; @@ -27,11 +30,18 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; @SuppressWarnings("BusyWait") @Service @@ -405,6 +415,11 @@ public void stopServer(final UserI user, final String servername, String eventTr JupyterServerEventI.Operation.Stop, 50, "Sending stop request to JupyterHub.")); + Optional serverForFolderDeletion = jupyterHubClient.getServer(user.getUsername(), servername); + if (serverForFolderDeletion.isPresent()) { + Server deletionServer = serverForFolderDeletion.get(); + FileUtils.removeCombinedFolder(Paths.get(JupyterHubPreferences.SHARED_PROJECT_STRING, deletionServer.getUser_options().get("eventTrackingId"))); + } jupyterHubClient.stopServer(user.getUsername(), servername); int time = 0; @@ -428,7 +443,7 @@ public void stopServer(final UserI user, final String servername, String eventTr eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), JupyterServerEventI.Operation.Stop, "Failed to stop Jupyter Server.")); - } catch (RuntimeException | InterruptedException e) { + } catch (RuntimeException | InterruptedException | IOException e) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), JupyterServerEventI.Operation.Stop, "Failed to stop Jupyter Server.")); @@ -532,6 +547,27 @@ public void cullLongRunningServers() { } } + /** + * Removes shared data associated with jupyter servers created for projects. This method will be called once per day + * to check whether any servers have closed unexpectedly resulting in hanging data within the archive. If so, this + * method will delete that data. + */ + public void cleanupOrphanedSharedDataDirs() { + List eventTrackingIds = jupyterHubClient.getUsers().stream().flatMap(user -> user.getServers().values() + .stream().map(server -> server.getUser_options().get("eventTrackingId"))).collect(Collectors.toList()); + Path baseDir = Paths.get(XDAT.getSiteConfigPreferences().getArchivePath(), FileUtils.SHARED_PROJECT_DIRECTORY_STRING, + JupyterHubPreferences.SHARED_PROJECT_STRING); + try { + List directories = Files.list(baseDir).filter(f -> !eventTrackingIds.contains(f.getFileName().toString())).collect(Collectors.toList()); + for (Path directory : directories) { + FileUtils.removeCombinedFolder(Paths.get(JupyterHubPreferences.SHARED_PROJECT_STRING, String.valueOf(directory.getFileName()))); + } + } catch (IOException e) { + log.error("Could not remove outdated directories: ", e); + } + + } + private Integer inMilliSec(Integer seconds) { return seconds * 1000; } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java index ce32243..126b23b 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.nrg.framework.constants.Scope; +import org.nrg.xdat.XDAT; import org.nrg.xdat.entities.AliasToken; import org.nrg.xdat.model.XnatSubjectassessordataI; import org.nrg.xdat.om.*; @@ -12,6 +13,7 @@ import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.SearchHelperServiceI; import org.nrg.xdat.services.AliasTokenService; +import org.nrg.xft.exception.DBPoolException; import org.nrg.xft.exception.ElementNotFoundException; import org.nrg.xft.exception.XFTInitException; import org.nrg.xft.schema.Wrappers.GenericWrapper.GenericWrapperElement; @@ -19,6 +21,7 @@ import org.nrg.xnat.compute.models.*; import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xnat.exceptions.InvalidArchiveStructure; +import org.nrg.xnat.utils.FileUtils; import org.nrg.xnatx.plugins.jupyterhub.entities.UserOptionsEntity; import org.nrg.xnatx.plugins.jupyterhub.models.XnatUserOptions; import org.nrg.xnatx.plugins.jupyterhub.models.docker.Mount; @@ -32,9 +35,11 @@ import org.springframework.stereotype.Service; import javax.annotation.Nullable; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.sql.SQLException; import java.util.*; import java.util.stream.Collectors; @@ -70,30 +75,53 @@ public DefaultUserOptionsService(final JupyterHubPreferences jupyterHubPreferenc this.jobTemplateService = jobTemplateService; } - @Override public Map getProjectPaths(final UserI user, final List projectIds) { + return getProjectPaths(user, projectIds, null); + } + @Override + public Map getProjectPaths(final UserI user, final List projectIds, @Nullable String eventTrackingId) { Map projectPaths = new HashMap<>(); projectIds.forEach(projectId -> { XnatProjectdata xnatProjectdata = XnatProjectdata.getXnatProjectdatasById(projectId, user, false); if (xnatProjectdata != null) { - // Experiments - final Path projectDirectory = Paths.get(xnatProjectdata.getRootArchivePath() + xnatProjectdata.getCurrentArc()); - if (Files.exists(projectDirectory)) { - projectPaths.put("/data/projects/" + projectId + "/experiments", projectDirectory.toString()); - } + //In this case we're creating a directory for both shared and standard data for a project as one folder + //should always be the case for basic project based needs not used for stored searches etc. + if (eventTrackingId != null) { + try { + Map allSharedPaths = FileUtils.getAllSharedPaths(projectId, user, true, true, true, true); + if (allSharedPaths.isEmpty()) { + final Path projectDirectory = Paths.get(xnatProjectdata.getRootArchivePath() + xnatProjectdata.getCurrentArc()); + if (Files.exists(projectDirectory)) { + projectPaths.put("/data/projects/" + projectId + "/experiments", projectDirectory.toString()); + } + } else { + Path sharedLinksDirectory = Paths.get(JupyterHubPreferences.SHARED_PROJECT_STRING, eventTrackingId); + projectPaths.put("/data/projects/" + projectId, String.valueOf(FileUtils.createDirectoryForSharedData(allSharedPaths, sharedLinksDirectory))); + } + } catch (Exception e) { + log.error("Unable to access shared data for the current project", e); + } + } else { + //here we're working with stored searches and the like + // Experiments + final Path projectDirectory = Paths.get(xnatProjectdata.getRootArchivePath() + xnatProjectdata.getCurrentArc()); + if (Files.exists(projectDirectory)) { + projectPaths.put("/data/projects/" + projectId + "/experiments", projectDirectory.toString()); + } - // Project resources - final Path resourceDirectory = Paths.get(xnatProjectdata.getRootArchivePath() + "/resources"); - if (Files.exists(resourceDirectory)) { - projectPaths.put("/data/projects/" + projectId + "/resources", resourceDirectory.toString()); - } + // Project resources + final Path resourceDirectory = Paths.get(xnatProjectdata.getRootArchivePath() + "/resources"); + if (Files.exists(resourceDirectory)) { + projectPaths.put("/data/projects/" + projectId + "/resources", resourceDirectory.toString()); + } - // Subject resources - final Path subjectResourceDirectory = Paths.get(xnatProjectdata.getRootArchivePath() + "/subjects"); - if (Files.exists(subjectResourceDirectory)) { - projectPaths.put("/data/projects/" + projectId + "/subjects", subjectResourceDirectory.toString()); + // Subject resources + final Path subjectResourceDirectory = Paths.get(xnatProjectdata.getRootArchivePath() + "/subjects"); + if (Files.exists(subjectResourceDirectory)) { + projectPaths.put("/data/projects/" + projectId + "/subjects", subjectResourceDirectory.toString()); + } } } @@ -304,7 +332,7 @@ public void storeUserOptions(UserI user, String servername, String xsiType, Stri switch (xsiType) { case (XnatProjectdata.SCHEMA_ELEMENT_NAME): { - paths.putAll(getProjectPaths(user, Collections.singletonList(id))); + paths.putAll(getProjectPaths(user, Collections.singletonList(id), eventTrackingId)); break; } case (XnatSubjectdata.SCHEMA_ELEMENT_NAME): { From fb7dfb134d0d6f70db4d0f8cfe5aa5ed06de6032 Mon Sep 17 00:00:00 2001 From: radiologics_ian Date: Mon, 23 Oct 2023 15:20:21 -0400 Subject: [PATCH 37/49] Fixing quick issue with exception handling. --- .../jupyterhub/services/impl/DefaultUserOptionsService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java index 126b23b..3aa128e 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java @@ -102,6 +102,7 @@ public Map getProjectPaths(final UserI user, final List } } catch (Exception e) { log.error("Unable to access shared data for the current project", e); + throw new RuntimeException(e); } } else { //here we're working with stored searches and the like From 08f38b91ebda44711644ed8e1450da703a655db0 Mon Sep 17 00:00:00 2001 From: radiologics_ian Date: Tue, 24 Oct 2023 12:58:08 -0400 Subject: [PATCH 38/49] Fixing the exception structure. --- .../services/UserOptionsService.java | 13 +++++++++---- .../impl/DefaultUserOptionsService.java | 18 ++++++------------ 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java index eedca21..ccd8003 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java @@ -1,26 +1,31 @@ package org.nrg.xnatx.plugins.jupyterhub.services; +import org.nrg.xdat.om.base.BaseXnatExperimentdata; +import org.nrg.xft.exception.DBPoolException; import org.nrg.xft.security.UserI; +import org.nrg.xnat.exceptions.InvalidArchiveStructure; import org.nrg.xnatx.plugins.jupyterhub.models.XnatUserOptions; +import java.io.IOException; +import java.sql.SQLException; import java.util.List; import java.util.Map; import java.util.Optional; public interface UserOptionsService { - Map getProjectPaths(UserI user, List projectIds); - Map getProjectPaths(UserI user, List projectIds, String eventTrackingId); + Map getProjectPaths(UserI user, List projectIds) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException; + Map getProjectPaths(UserI user, List projectIds, String eventTrackingId) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException; Map getSubjectPaths(UserI user, String subjectId); Map getSubjectPaths(UserI user, List subjectIds); Map getExperimentPath(UserI user, String experimentId); Map getExperimentPaths(UserI user, List experimentIds); Map getImageScanPath(UserI user, Integer imageScanId); Map getImageScanPaths(UserI user, List imageScanIds); - Map getStoredSearchPaths(UserI user, String storedSearchId, String projectId); + Map getStoredSearchPaths(UserI user, String storedSearchId, String projectId) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException; Optional retrieveUserOptions(UserI user); Optional retrieveUserOptions(UserI user, String servername); - void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, Long computeEnvironmentConfigId, Long hardwareConfigId, String eventTrackingId); + void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, Long computeEnvironmentConfigId, Long hardwareConfigId, String eventTrackingId) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException; } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java index 3aa128e..3b95a76 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java @@ -75,21 +75,20 @@ public DefaultUserOptionsService(final JupyterHubPreferences jupyterHubPreferenc this.jobTemplateService = jobTemplateService; } - public Map getProjectPaths(final UserI user, final List projectIds) { + public Map getProjectPaths(final UserI user, final List projectIds) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException { return getProjectPaths(user, projectIds, null); } @Override - public Map getProjectPaths(final UserI user, final List projectIds, @Nullable String eventTrackingId) { + public Map getProjectPaths(final UserI user, final List projectIds, @Nullable String eventTrackingId) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException { Map projectPaths = new HashMap<>(); - projectIds.forEach(projectId -> { + for (String projectId: projectIds) { XnatProjectdata xnatProjectdata = XnatProjectdata.getXnatProjectdatasById(projectId, user, false); if (xnatProjectdata != null) { //In this case we're creating a directory for both shared and standard data for a project as one folder //should always be the case for basic project based needs not used for stored searches etc. if (eventTrackingId != null) { - try { Map allSharedPaths = FileUtils.getAllSharedPaths(projectId, user, true, true, true, true); if (allSharedPaths.isEmpty()) { final Path projectDirectory = Paths.get(xnatProjectdata.getRootArchivePath() + xnatProjectdata.getCurrentArc()); @@ -100,10 +99,6 @@ public Map getProjectPaths(final UserI user, final List Path sharedLinksDirectory = Paths.get(JupyterHubPreferences.SHARED_PROJECT_STRING, eventTrackingId); projectPaths.put("/data/projects/" + projectId, String.valueOf(FileUtils.createDirectoryForSharedData(allSharedPaths, sharedLinksDirectory))); } - } catch (Exception e) { - log.error("Unable to access shared data for the current project", e); - throw new RuntimeException(e); - } } else { //here we're working with stored searches and the like // Experiments @@ -126,8 +121,7 @@ public Map getProjectPaths(final UserI user, final List } } - }); - + } return projectPaths; } @@ -237,7 +231,7 @@ public Map getImageScanPaths(final UserI user, final List getStoredSearchPaths(UserI user, String storedSearchId, @Nullable String projectId) { + public Map getStoredSearchPaths(UserI user, String storedSearchId, @Nullable String projectId) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException { Map storedSearchPaths = new HashMap<>(); XdatStoredSearch storedSearch; @@ -308,7 +302,7 @@ public Optional retrieveUserOptions(UserI user, String serverna @Override public void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, - Long computeEnvironmentConfigId, Long hardwareConfigId, String eventTrackingId) { + Long computeEnvironmentConfigId, Long hardwareConfigId, String eventTrackingId) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException { log.debug("Storing user options for user '{}' server '{}' xsiType '{}' id '{}' projectId '{}'", user.getUsername(), servername, xsiType, id, projectId); From 91127bb1b121f6d12da94b00dce54a20c612e4b5 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Fri, 27 Oct 2023 10:21:35 -0500 Subject: [PATCH 39/49] JHP-70: Add capability to configure plugin preferences using environment variables at startup. --- .../JupyterHubPreferenceInitializer.java | 238 +++++++++++++++++- .../initialize/JupyterHubUserInitializer.java | 23 +- .../preferences/JupyterHubPreferences.java | 13 +- .../jupyterhub/utils/SystemHelper.java | 4 + .../JupyterHubUserInitializerConfig.java | 15 +- .../JupyterHubPreferenceInitializerTest.java | 105 +++++++- .../JupyterHubUserInitializerTest.java | 51 ++++ .../utils/impl/DefaultSystemHelperTest.java | 7 +- 8 files changed, 413 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializer.java index 4f0fd4b..b69b4f5 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializer.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializer.java @@ -1,6 +1,7 @@ package org.nrg.xnatx.plugins.jupyterhub.initialize; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; import org.nrg.xnat.initialization.tasks.InitializingTaskException; @@ -56,27 +57,240 @@ protected void callImpl() throws InitializingTaskException { throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); } - // Initialize the JupyterHub preferences. + // Initialize the JupyterHub plugin preferences. initializeJupyterHubHostUrl(); + initializeJupyterHubApiUrlFromEnv(); + initializeJupyterHubTokenFromEnv(); + initializeStartTimeoutFromEnv(); + initializeStopTimeoutFromEnv(); + initializePathTranslationArchivePrefixFromEnv(); + initializePathTranslationArchiveDockerPrefixFromEnv(); + initializePathTranslationWorkspacePrefixFromEnv(); + initializePathTranslationWorkspaceDockerPrefixFromEnv(); + initializeWorkspacePathFromEnv(); + initializeInactivityTimeoutFromEnv(); + initializeMaxServerLifetimeFromEnv(); + initializeAllUsersCanStartJupyterFromEnv(); } /** * Initialize the JupyterHub Host URL preference. - * If the JupyterHub Host URL preference is not set, check for the JH_HOST_URL environment variable. If the - * environment variable is not set, default to the XNAT site url. + *

    + * The JupyterHub Host URL preference is initialized in the following order: + * 1. If the JH_XNAT_JUPYTERHUB_HOST_URL environment variable is set, use that. + * 2. If the JupyterHub Host URL preference is not set, try to set it to the XNAT site url. + * 3. If the JupyterHub Host URL preference is already set, do nothing. */ protected void initializeJupyterHubHostUrl() { - // Check if the JupyterHub host url preference is set. - if (jupyterHubPreferences.getJupyterHubHostUrl() == null || jupyterHubPreferences.getJupyterHubHostUrl().isEmpty()) { - // Check for env variable or default to site url. - String jupyterHubHostUrl = systemHelper.getEnv("JH_HOST_URL"); - if (jupyterHubHostUrl == null || jupyterHubHostUrl.isEmpty()) { - jupyterHubHostUrl = siteConfigPreferences.getSiteUrl(); - } + String jupyterHubHostUrl = systemHelper.getEnv("JH_XNAT_JUPYTERHUB_HOST_URL"); + + if (StringUtils.isNotBlank(jupyterHubHostUrl)) { + // Try to set from the environment variable. jupyterHubPreferences.setJupyterHubHostUrl(jupyterHubHostUrl); - log.info("JupyterHub host url initialized to: " + jupyterHubHostUrl); + log.info("JupyterHub Host URL initialized from environment variable to: " + jupyterHubHostUrl); + } else if (StringUtils.isBlank(jupyterHubPreferences.getJupyterHubHostUrl())) { + // Try to set from the XNAT site url. + String siteUrl = siteConfigPreferences.getSiteUrl(); + if (StringUtils.isNotBlank(siteUrl)) { + jupyterHubPreferences.setJupyterHubHostUrl(siteUrl); + log.info("JupyterHub Host URL preference initialized to site URL: " + siteUrl); + } else { + log.info("JupyterHub Host URL not set and XNAT site url not set. Skipping initialization."); + } + } else { + log.info("JupyterHub Host URL already set. Skipping initialization."); + } + } + + /** + * Initialize the JupyterHub API URL preference from the JH_XNAT_JUPYTERHUB_API_URL environment variable. + */ + protected void initializeJupyterHubApiUrlFromEnv() { + String jupyterHubApiUrlEnv = systemHelper.getEnv("JH_XNAT_JUPYTERHUB_API_URL"); + + if (StringUtils.isNotBlank(jupyterHubApiUrlEnv)) { + jupyterHubPreferences.setJupyterHubApiUrl(jupyterHubApiUrlEnv); + log.info("JupyterHub API URL preference initialized from environment variable to: " + jupyterHubApiUrlEnv); + } else { + log.debug("JupyterHub API URL environment variable not set. Skipping initialization."); + } + } + + /** + * Initialize the JupyterHub token preference from the JH_XNAT_SERVICE_TOKEN environment variable. + */ + protected void initializeJupyterHubTokenFromEnv() { + String jupyterHubTokenEnv = systemHelper.getEnv("JH_XNAT_SERVICE_TOKEN"); + + if (StringUtils.isNotBlank(jupyterHubTokenEnv)) { + jupyterHubPreferences.setJupyterHubToken(jupyterHubTokenEnv); + log.info("JupyterHub Token preference initialized from environment variable."); + } else { + log.debug("JupyterHub Token environment variable not set. Skipping initialization."); + } + } + + /** + * Initialize the JupyterHub start timeout preference from the JH_XNAT_START_TIMEOUT environment variable. + */ + protected void initializeStartTimeoutFromEnv() { + String jupyterHubStartTimeoutEnv = systemHelper.getEnv("JH_XNAT_START_TIMEOUT"); + + if (StringUtils.isNotBlank(jupyterHubStartTimeoutEnv)) { + try { // Try to convert to an integer. + int jupyterHubStartTimeout = Integer.parseInt(jupyterHubStartTimeoutEnv); + jupyterHubPreferences.setStartTimeout(jupyterHubStartTimeout); + log.info("JupyterHub Start Timeout preference initialized from environment variable to: " + jupyterHubStartTimeout); + } catch (NumberFormatException e) { + log.error("JupyterHub Start Timeout environment variable is not an integer. Skipping initialization."); + } + } else { + log.debug("JupyterHub Start Timeout environment variable not set. Skipping initialization."); + } + } + + /** + * Initialize the JupyterHub stop timeout preference from the JH_XNAT_STOP_TIMEOUT environment variable. + */ + protected void initializeStopTimeoutFromEnv() { + String jupyterHubStopTimeoutEnv = systemHelper.getEnv("JH_XNAT_STOP_TIMEOUT"); + + if (StringUtils.isNotBlank(jupyterHubStopTimeoutEnv)) { + try { // Try to convert to an integer. + int jupyterHubStopTimeout = Integer.parseInt(jupyterHubStopTimeoutEnv); + jupyterHubPreferences.setStopTimeout(jupyterHubStopTimeout); + log.info("JupyterHub Stop Timeout preference initialized from environment variable to: " + jupyterHubStopTimeout); + } catch (NumberFormatException e) { + log.error("JupyterHub stop timeout environment variable is not an integer. Skipping initialization."); + } + } else { + log.debug("JupyterHub Stop Timeout environment variable not set. Skipping initialization."); + } + } + + /** + * Initialize the JupyterHub path translation archive prefix preference from the JH_XNAT_PT_ARCHIVE_PREFIX environment variable. + */ + protected void initializePathTranslationArchivePrefixFromEnv() { + String jupyterHubPathTranslationArchivePrefixEnv = systemHelper.getEnv("JH_XNAT_PT_ARCHIVE_PREFIX"); + + if (StringUtils.isNotBlank(jupyterHubPathTranslationArchivePrefixEnv)) { + jupyterHubPreferences.setPathTranslationArchivePrefix(jupyterHubPathTranslationArchivePrefixEnv); + log.info("JupyterHub Path Translation Archive Prefix preference initialized from environment variable to: " + jupyterHubPathTranslationArchivePrefixEnv); + } else { + log.debug("JupyterHub Path Translation Archive Prefix environment variable not set. Skipping initialization."); + } + } + + /** + * Initialize the JupyterHub path translation archive docker prefix preference from the JH_XNAT_PT_ARCHIVE_DOCKER_PREFIX environment variable. + */ + protected void initializePathTranslationArchiveDockerPrefixFromEnv() { + String jupyterHubPathTranslationArchiveDockerPrefixEnv = systemHelper.getEnv("JH_XNAT_PT_ARCHIVE_DOCKER_PREFIX"); + + if (StringUtils.isNotBlank(jupyterHubPathTranslationArchiveDockerPrefixEnv)) { + jupyterHubPreferences.setPathTranslationArchiveDockerPrefix(jupyterHubPathTranslationArchiveDockerPrefixEnv); + log.info("JupyterHub Path Translation Archive Docker Prefix preference initialized from environment variable to: " + jupyterHubPathTranslationArchiveDockerPrefixEnv); + } else { + log.debug("JupyterHub Path Translation Archive Docker Prefix environment variable not set. Skipping initialization."); + } + } + + /** + * Initialize the JupyterHub path translation workspace prefix preference from the JH_XNAT_PT_WORKSPACE_PREFIX environment variable. + */ + protected void initializePathTranslationWorkspacePrefixFromEnv() { + String jupyterHubPathTranslationWorkspacePrefixEnv = systemHelper.getEnv("JH_XNAT_PT_WORKSPACE_PREFIX"); + + if (StringUtils.isNotBlank(jupyterHubPathTranslationWorkspacePrefixEnv)) { + jupyterHubPreferences.setPathTranslationWorkspacePrefix(jupyterHubPathTranslationWorkspacePrefixEnv); + log.info("JupyterHub Path Translation Workspace Prefix preference initialized from environment variable to: " + jupyterHubPathTranslationWorkspacePrefixEnv); + } else { + log.debug("JupyterHub Path Translation Workspace Prefix environment variable not set. Skipping initialization."); + } + } + + /** + * Initialize the JupyterHub path translation workspace docker prefix preference from the JH_XNAT_PT_WORKSPACE_DOCKER_PREFIX environment variable. + */ + protected void initializePathTranslationWorkspaceDockerPrefixFromEnv() { + String jupyterHubPathTranslationWorkspaceDockerPrefixEnv = systemHelper.getEnv("JH_XNAT_PT_WORKSPACE_DOCKER_PREFIX"); + + if (StringUtils.isNotBlank(jupyterHubPathTranslationWorkspaceDockerPrefixEnv)) { + jupyterHubPreferences.setPathTranslationWorkspaceDockerPrefix(jupyterHubPathTranslationWorkspaceDockerPrefixEnv); + log.info("JupyterHub Path Translation Workspace Docker Prefix preference initialized from environment variable to: " + jupyterHubPathTranslationWorkspaceDockerPrefixEnv); + } else { + log.debug("JupyterHub Path Translation Workspace Docker Prefix environment variable not set. Skipping initialization."); + } + } + + /** + * Initialize the JupyterHub workspace path preference from the JH_XNAT_WORKSPACE_PATH environment variable. + */ + protected void initializeWorkspacePathFromEnv() { + String jupyterHubWorkspacePathEnv = systemHelper.getEnv("JH_XNAT_WORKSPACE_PATH"); + + if (StringUtils.isNotBlank(jupyterHubWorkspacePathEnv)) { + jupyterHubPreferences.setWorkspacePath(jupyterHubWorkspacePathEnv); + log.info("JupyterHub Workspace Path preference initialized from environment variable to: " + jupyterHubWorkspacePathEnv); + } else { + log.debug("JupyterHub Workspace Path environment variable not set. Skipping initialization."); + } + } + + /** + * Initialize the JupyterHub inactivity timeout preference from the JH_XNAT_INACTIVITY_TIMEOUT environment variable. + */ + protected void initializeInactivityTimeoutFromEnv() { + String jupyterHubInactivityTimeoutEnv = systemHelper.getEnv("JH_XNAT_INACTIVITY_TIMEOUT"); + + if (StringUtils.isNotBlank(jupyterHubInactivityTimeoutEnv)) { + try { // Try to convert to a long + long jupyterHubInactivityTimeout = Long.parseLong(jupyterHubInactivityTimeoutEnv); + jupyterHubPreferences.setInactivityTimeout(jupyterHubInactivityTimeout); + log.info("JupyterHub Inactivity Timeout preference initialized from environment variable to: " + jupyterHubInactivityTimeout); + } catch (NumberFormatException e) { + log.error("JupyterHub Inactivity Timeout environment variable is not a long. Skipping initialization."); + } } else { - log.info("JupyterHub host url already set. Skipping initialization."); + log.debug("JupyterHub Inactivity Timeout environment variable not set. Skipping initialization."); } } + + /** + * Initialize the JupyterHub max server lifetime preference from the JH_XNAT_MAX_SERVER_LIFETIME environment variable. + */ + protected void initializeMaxServerLifetimeFromEnv() { + String jupyterHubMaxServerLifetimeEnv = systemHelper.getEnv("JH_XNAT_MAX_SERVER_LIFETIME"); + + if (StringUtils.isNotBlank(jupyterHubMaxServerLifetimeEnv)) { + try { // Try to convert to a long + long jupyterHubMaxServerLifetime = Long.parseLong(jupyterHubMaxServerLifetimeEnv); + // If it is set, set the max server lifetime preference. + jupyterHubPreferences.setMaxServerLifetime(jupyterHubMaxServerLifetime); + log.info("JupyterHub Max Server Lifetime preference initialized from environment variable to: " + jupyterHubMaxServerLifetime); + } catch (NumberFormatException e) { + log.error("JupyterHub Max Server Lifetime environment variable is not a long. Skipping initialization."); + } + } else { + log.debug("JupyterHub Max Server Lifetime environment variable not set. Skipping initialization."); + } + } + + /** + * Initialize the JupyterHub all users can start Jupyter preference from the JH_XNAT_ALL_USERS_CAN_START_JUPYTER environment variable. + */ + protected void initializeAllUsersCanStartJupyterFromEnv() { + String jupyterHubAllUsersCanStartJupyterEnv = systemHelper.getEnv("JH_XNAT_ALL_USERS_CAN_START_JUPYTER"); + + if (StringUtils.isNotBlank(jupyterHubAllUsersCanStartJupyterEnv)) { + // Convert to a boolean, string must be "true" or anything else is false. + boolean jupyterHubAllUsersCanStartJupyter = Boolean.parseBoolean(jupyterHubAllUsersCanStartJupyterEnv); + jupyterHubPreferences.setAllUsersCanStartJupyter(jupyterHubAllUsersCanStartJupyter); + log.info("JupyterHub All Users Can Start Jupyter preference initialized from environment variable to: " + jupyterHubAllUsersCanStartJupyter); + } else { + log.debug("JupyterHub All Users Can Start Jupyter environment variable not set. Skipping initialization."); + } + } + } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java index eff1c4c..fb71a35 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializer.java @@ -11,6 +11,7 @@ import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; import org.nrg.xnat.initialization.tasks.InitializingTaskException; import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.utils.SystemHelper; import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -26,16 +27,19 @@ public class JupyterHubUserInitializer extends AbstractInitializingTask { private final RoleHolder roleHolder; private final XFTManagerHelper xftManagerHelper; private final XnatAppInfo appInfo; + private final SystemHelper systemHelper; @Autowired public JupyterHubUserInitializer(final UserManagementServiceI userManagementService, final RoleHolder roleHolder, final XFTManagerHelper xftManagerHelper, - final XnatAppInfo appInfo) { + final XnatAppInfo appInfo, + final SystemHelper systemHelper) { this.userManagementService = userManagementService; this.roleHolder = roleHolder; this.xftManagerHelper = xftManagerHelper; this.appInfo = appInfo; + this.systemHelper = systemHelper; } @Override @@ -62,18 +66,22 @@ protected void callImpl() throws InitializingTaskException { throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); } - if (userManagementService.exists("jupyterhub")) { - log.info("`jupyterhub` user account already exists, skipping creation."); + final String username = systemHelper.getOrDefault("JH_XNAT_USERNAME", "jupyterhub"); + final String password = systemHelper.getOrDefault("JH_XNAT_PASSWORD", "jupyterhub"); + final boolean enabled = systemHelper.getOrDefault("JH_XNAT_SERVICE_USER_ENABLED", "false").equals("true"); + + if (userManagementService.exists(username)) { + log.info("`jupyterhub` user account {} already exists, skipping creation.", username); return; } final UserI jupyterhubUser = userManagementService.createUser(); - jupyterhubUser.setLogin("jupyterhub"); + jupyterhubUser.setLogin(username); jupyterhubUser.setEmail("jupyterhub@jupyterhub.jupyterhub"); jupyterhubUser.setFirstname("jupyterhub"); jupyterhubUser.setLastname("jupyterhub"); - jupyterhubUser.setPassword("jupyterhub"); - jupyterhubUser.setEnabled(false); + jupyterhubUser.setPassword(password); + jupyterhubUser.setEnabled(enabled); jupyterhubUser.setVerified(true); String errorMessage = "Unable to create the `jupyterhub` user account. " + @@ -108,7 +116,8 @@ false, new EventDetails(EventUtils.CATEGORY.DATA, throw new InitializingTaskException(InitializingTaskException.Level.Error, errorMessage, e); } - log.info("Successfully created `jupyterhub` user account."); + log.info("Successfully created `jupyterhub` user account: username={}, password=HIDDEN, enabled={}", + username, enabled); } @Override diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java index affab88..cee7b4d 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/preferences/JupyterHubPreferences.java @@ -41,6 +41,9 @@ protected JupyterHubPreferences(NrgPreferenceService preferenceService, ConfigPa super(preferenceService, configFolderPaths, initPrefs); } + /** + * Defaults to empty string. This preference is dynamically set to the site url in JupyterHubPreferenceInitializer. + */ @NrgPreference(defaultValue = "") public String getJupyterHubHostUrl() { return getValue(JUPYTERHUB_HOST_URL); @@ -176,11 +179,6 @@ public void setPathTranslationArchiveDockerPrefix(final String pathTranslationAr } } - @NrgPreference(defaultValue = "/data/xnat/workspaces") - public String getWorkspacePath() { - return getValue("workspacePath"); - } - @NrgPreference(defaultValue = "") public String getPathTranslationWorkspacePrefix() { return getValue("pathTranslationWorkspacePrefix"); @@ -207,6 +205,11 @@ public void setPathTranslationWorkspaceDockerPrefix(final String pathTranslation } } + @NrgPreference(defaultValue = "/data/xnat/workspaces") + public String getWorkspacePath() { + return getValue("workspacePath"); + } + public void setWorkspacePath(final String workspacePath) { try { set(workspacePath, "workspacePath"); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/SystemHelper.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/SystemHelper.java index 81f2a2f..fdf6a01 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/SystemHelper.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/utils/SystemHelper.java @@ -6,4 +6,8 @@ default String getEnv(String name) { return System.getenv(name); } + default String getOrDefault(String name, String defaultValue) { + return System.getenv().getOrDefault(name, defaultValue); + } + } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubUserInitializerConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubUserInitializerConfig.java index 84f3a58..14ae81c 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubUserInitializerConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubUserInitializerConfig.java @@ -4,6 +4,7 @@ import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnatx.plugins.jupyterhub.initialize.JupyterHubUserInitializer; +import org.nrg.xnatx.plugins.jupyterhub.utils.SystemHelper; import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -17,11 +18,15 @@ public class JupyterHubUserInitializerConfig { public JupyterHubUserInitializer defaultJupyterHubUserInitializer(final UserManagementServiceI mockUserManagementService, final RoleHolder mockRoleHolder, final XFTManagerHelper mockXFTManagerHelper, - final XnatAppInfo mockXnatAppInfo) { - return new JupyterHubUserInitializer(mockUserManagementService, - mockRoleHolder, - mockXFTManagerHelper, - mockXnatAppInfo); + final XnatAppInfo mockXnatAppInfo, + final SystemHelper mockSystemHelper) { + return new JupyterHubUserInitializer( + mockUserManagementService, + mockRoleHolder, + mockXFTManagerHelper, + mockXnatAppInfo, + mockSystemHelper + ); } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializerTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializerTest.java index b7e243b..028bbb0 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializerTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubPreferenceInitializerTest.java @@ -91,48 +91,69 @@ public void callImpl_notInitialized2() throws Exception { } @Test - public void callImpl_initialized() throws Exception { + public void callImpl_jhHostUrlAlreadySet() throws Exception { // Setup when(mockXFTManagerHelper.isInitialized()).thenReturn(true); when(mockXnatAppInfo.isInitialized()).thenReturn(true); + when(mockJupyterHubPreferences.getJupyterHubHostUrl()).thenReturn("https://my-xnat.com"); // Test jupyterHubPreferenceInitializer.callImpl(); // Verify - verify(mockJupyterHubPreferences, times(1)).setJupyterHubHostUrl(any()); + verify(mockJupyterHubPreferences, never()).setJupyterHubHostUrl(any()); } @Test - public void callImpl_initialized_alreadySet() throws Exception { + public void callImpl_jhHostUrlSetToSiteUrl() throws Exception { // Setup when(mockXFTManagerHelper.isInitialized()).thenReturn(true); when(mockXnatAppInfo.isInitialized()).thenReturn(true); - when(mockJupyterHubPreferences.getJupyterHubHostUrl()).thenReturn("https://my-xnat.com"); + when(mockSiteConfigPreferences.getSiteUrl()).thenReturn("https://my-xnat.com"); // Test jupyterHubPreferenceInitializer.callImpl(); // Verify - verify(mockJupyterHubPreferences, never()).setJupyterHubHostUrl(any()); + verify(mockJupyterHubPreferences).setJupyterHubHostUrl("https://my-xnat.com"); } - @Test public void callImpl_initialized_envSet() throws Exception { // Setup when(mockXFTManagerHelper.isInitialized()).thenReturn(true); when(mockXnatAppInfo.isInitialized()).thenReturn(true); - when(mockJupyterHubPreferences.getJupyterHubHostUrl()).thenReturn(""); - when(mockSystemHelper.getEnv("JH_HOST_URL")).thenReturn("https://my-xnat.com"); - when(mockSiteConfigPreferences.getSiteUrl()).thenReturn("https://another-xnat.com"); + when(mockSystemHelper.getEnv("JH_XNAT_JUPYTERHUB_HOST_URL")).thenReturn("https://my-xnat.com"); + when(mockSystemHelper.getEnv("JH_XNAT_JUPYTERHUB_API_URL")).thenReturn("https://my-xnat.com/jupyterhub/hub/api"); + when(mockSystemHelper.getEnv("JH_XNAT_SERVICE_TOKEN")).thenReturn("secret-token"); + when(mockSystemHelper.getEnv("JH_XNAT_START_TIMEOUT")).thenReturn("60"); + when(mockSystemHelper.getEnv("JH_XNAT_STOP_TIMEOUT")).thenReturn("60"); + when(mockSystemHelper.getEnv("JH_XNAT_PT_ARCHIVE_PREFIX")).thenReturn("/data/xnat/archive"); + when(mockSystemHelper.getEnv("JH_XNAT_PT_ARCHIVE_DOCKER_PREFIX")).thenReturn("/home/andy/xnat-docker-compose/xnat-data/archive"); + when(mockSystemHelper.getEnv("JH_XNAT_PT_WORKSPACE_PREFIX")).thenReturn("/data/xnat/workspaces"); + when(mockSystemHelper.getEnv("JH_XNAT_PT_WORKSPACE_DOCKER_PREFIX")).thenReturn("/home/andy/xnat-docker-compose/xnat-data/workspaces"); + when(mockSystemHelper.getEnv("JH_XNAT_WORKSPACE_PATH")).thenReturn("/data/xnat/workspaces"); + when(mockSystemHelper.getEnv("JH_XNAT_INACTIVITY_TIMEOUT")).thenReturn("24"); + when(mockSystemHelper.getEnv("JH_XNAT_MAX_SERVER_LIFETIME")).thenReturn("48"); + when(mockSystemHelper.getEnv("JH_XNAT_ALL_USERS_CAN_START_JUPYTER")).thenReturn("true"); // Test jupyterHubPreferenceInitializer.callImpl(); // Verify verify(mockJupyterHubPreferences).setJupyterHubHostUrl("https://my-xnat.com"); + verify(mockJupyterHubPreferences).setJupyterHubApiUrl("https://my-xnat.com/jupyterhub/hub/api"); + verify(mockJupyterHubPreferences).setJupyterHubToken("secret-token"); + verify(mockJupyterHubPreferences).setStartTimeout(60); + verify(mockJupyterHubPreferences).setStopTimeout(60); + verify(mockJupyterHubPreferences).setPathTranslationArchivePrefix("/data/xnat/archive"); + verify(mockJupyterHubPreferences).setPathTranslationArchiveDockerPrefix("/home/andy/xnat-docker-compose/xnat-data/archive"); + verify(mockJupyterHubPreferences).setPathTranslationWorkspacePrefix("/data/xnat/workspaces"); + verify(mockJupyterHubPreferences).setPathTranslationWorkspaceDockerPrefix("/home/andy/xnat-docker-compose/xnat-data/workspaces"); + verify(mockJupyterHubPreferences).setWorkspacePath("/data/xnat/workspaces"); + verify(mockJupyterHubPreferences).setInactivityTimeout(24L); + verify(mockJupyterHubPreferences).setMaxServerLifetime(48L); } @Test @@ -140,16 +161,74 @@ public void callImpl_initialized_envNotSet() throws Exception { // Setup when(mockXFTManagerHelper.isInitialized()).thenReturn(true); when(mockXnatAppInfo.isInitialized()).thenReturn(true); - when(mockJupyterHubPreferences.getJupyterHubHostUrl()).thenReturn(""); - when(mockSystemHelper.getEnv("JH_HOST_URL")).thenReturn(""); - when(mockSiteConfigPreferences.getSiteUrl()).thenReturn("https://another-xnat.com"); + when(mockSystemHelper.getEnv("JH_XNAT_JUPYTERHUB_HOST_URL")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_JUPYTERHUB_API_URL")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_SERVICE_TOKEN")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_START_TIMEOUT")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_STOP_TIMEOUT")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_PT_ARCHIVE_PREFIX")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_PT_ARCHIVE_DOCKER_PREFIX")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_PT_WORKSPACE_PREFIX")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_PT_WORKSPACE_DOCKER_PREFIX")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_WORKSPACE_PATH")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_INACTIVITY_TIMEOUT")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_MAX_SERVER_LIFETIME")).thenReturn(""); + when(mockSystemHelper.getEnv("JH_XNAT_ALL_USERS_CAN_START_JUPYTER")).thenReturn(""); + + // Test + jupyterHubPreferenceInitializer.callImpl(); + + // Verify + verify(mockJupyterHubPreferences, never()).setJupyterHubHostUrl(any()); + verify(mockJupyterHubPreferences, never()).setJupyterHubApiUrl(any()); + verify(mockJupyterHubPreferences, never()).setJupyterHubToken(any()); + verify(mockJupyterHubPreferences, never()).setStartTimeout(anyInt()); + verify(mockJupyterHubPreferences, never()).setStopTimeout(anyInt()); + verify(mockJupyterHubPreferences, never()).setPathTranslationArchivePrefix(any()); + verify(mockJupyterHubPreferences, never()).setPathTranslationArchiveDockerPrefix(any()); + verify(mockJupyterHubPreferences, never()).setPathTranslationWorkspacePrefix(any()); + verify(mockJupyterHubPreferences, never()).setPathTranslationWorkspaceDockerPrefix(any()); + verify(mockJupyterHubPreferences, never()).setWorkspacePath(any()); + verify(mockJupyterHubPreferences, never()).setInactivityTimeout(anyLong()); + verify(mockJupyterHubPreferences, never()).setMaxServerLifetime(anyLong()); + } + + @Test + public void callImpl_initialized_envNotSet2() throws Exception { + // Setup + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + + when(mockSystemHelper.getEnv("JH_XNAT_JUPYTERHUB_HOST_URL")).thenReturn(null); + when(mockSystemHelper.getEnv("JH_XNAT_JUPYTERHUB_API_URL")).thenReturn(null); + when(mockSystemHelper.getEnv("JH_XNAT_SERVICE_TOKEN")).thenReturn(null); + when(mockSystemHelper.getEnv("JH_XNAT_START_TIMEOUT")).thenReturn(null); + when(mockSystemHelper.getEnv("JH_XNAT_STOP_TIMEOUT")).thenReturn(null); + when(mockSystemHelper.getEnv("JH_XNAT_PT_ARCHIVE_PREFIX")).thenReturn(null); + when(mockSystemHelper.getEnv("JH_XNAT_PT_ARCHIVE_DOCKER_PREFIX")).thenReturn(null); + when(mockSystemHelper.getEnv("JH_XNAT_PT_WORKSPACE_PREFIX")).thenReturn(null); + when(mockSystemHelper.getEnv("JH_XNAT_PT_WORKSPACE_DOCKER_PREFIX")).thenReturn(null); + when(mockSystemHelper.getEnv("JH_XNAT_WORKSPACE_PATH")).thenReturn(null); + when(mockSystemHelper.getEnv("JH_XNAT_INACTIVITY_TIMEOUT")).thenReturn(null); + when(mockSystemHelper.getEnv("JH_XNAT_MAX_SERVER_LIFETIME")).thenReturn(null); // Test jupyterHubPreferenceInitializer.callImpl(); // Verify - verify(mockJupyterHubPreferences).setJupyterHubHostUrl("https://another-xnat.com"); + verify(mockJupyterHubPreferences, never()).setJupyterHubHostUrl(any()); + verify(mockJupyterHubPreferences, never()).setJupyterHubApiUrl(any()); + verify(mockJupyterHubPreferences, never()).setJupyterHubToken(any()); + verify(mockJupyterHubPreferences, never()).setStartTimeout(anyInt()); + verify(mockJupyterHubPreferences, never()).setStopTimeout(anyInt()); + verify(mockJupyterHubPreferences, never()).setPathTranslationArchivePrefix(any()); + verify(mockJupyterHubPreferences, never()).setPathTranslationArchiveDockerPrefix(any()); + verify(mockJupyterHubPreferences, never()).setPathTranslationWorkspacePrefix(any()); + verify(mockJupyterHubPreferences, never()).setPathTranslationWorkspaceDockerPrefix(any()); + verify(mockJupyterHubPreferences, never()).setWorkspacePath(any()); + verify(mockJupyterHubPreferences, never()).setInactivityTimeout(anyLong()); + verify(mockJupyterHubPreferences, never()).setMaxServerLifetime(anyLong()); } } \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java index c48fcb8..c049122 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/JupyterHubUserInitializerTest.java @@ -12,6 +12,7 @@ import org.nrg.xnat.initialization.tasks.InitializingTaskException; import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubUserInitializerConfig; +import org.nrg.xnatx.plugins.jupyterhub.utils.SystemHelper; import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; @@ -29,6 +30,7 @@ public class JupyterHubUserInitializerTest { @Autowired private XFTManagerHelper mockXFTManagerHelper; @Autowired private XnatAppInfo mockXnatAppInfo; @Autowired private RoleServiceI mockRoleService; + @Autowired private SystemHelper mockSystemHelper; private final String username = "jupyterhub"; @@ -38,6 +40,7 @@ public void after() { Mockito.reset(mockXFTManagerHelper); Mockito.reset(mockRoleService); Mockito.reset(mockXnatAppInfo); + Mockito.reset(mockSystemHelper); } @Test(expected = InitializingTaskException.class) @@ -67,6 +70,10 @@ public void test_JHUserAlreadyExists() throws Exception { when(mockXnatAppInfo.isInitialized()).thenReturn(true); when(mockUserManagementService.exists(username)).thenReturn(true); + when(mockSystemHelper.getOrDefault("JH_XNAT_USERNAME", "jupyterhub")).thenReturn("jupyterhub"); + when(mockSystemHelper.getOrDefault("JH_XNAT_PASSWORD", "jupyterhub")).thenReturn("jupyterhub"); + when(mockSystemHelper.getOrDefault("JH_XNAT_SERVICE_USER_ENABLED", "false")).thenReturn("false"); + // Test jupyterHubUserInitializer.callImpl(); @@ -86,6 +93,10 @@ public void test_FailedToSaveUser() throws Exception { doThrow(Exception.class).when(mockUserManagementService).save(any(UserI.class), nullable(UserI.class), anyBoolean(), any(EventDetails.class)); + when(mockSystemHelper.getOrDefault("JH_XNAT_USERNAME", "jupyterhub")).thenReturn("jupyterhub"); + when(mockSystemHelper.getOrDefault("JH_XNAT_PASSWORD", "jupyterhub")).thenReturn("jupyterhub"); + when(mockSystemHelper.getOrDefault("JH_XNAT_SERVICE_USER_ENABLED", "false")).thenReturn("false"); + // Test jupyterHubUserInitializer.callImpl(); @@ -104,6 +115,10 @@ public void test_FailedAddUserRole() throws Exception { when(mockUserManagementService.createUser()).thenReturn(mock(UserI.class)); when(mockRoleService.addRole(nullable(UserI.class), any(UserI.class), anyString())).thenReturn(false); + when(mockSystemHelper.getOrDefault("JH_XNAT_USERNAME", "jupyterhub")).thenReturn("jupyterhub"); + when(mockSystemHelper.getOrDefault("JH_XNAT_PASSWORD", "jupyterhub")).thenReturn("jupyterhub"); + when(mockSystemHelper.getOrDefault("JH_XNAT_SERVICE_USER_ENABLED", "false")).thenReturn("false"); + // Test jupyterHubUserInitializer.callImpl(); @@ -124,6 +139,10 @@ public void test_FailedAddUserRole_Exception() throws Exception { when(mockUserManagementService.createUser()).thenReturn(mock(UserI.class)); doThrow(Exception.class).when(mockRoleService).addRole(nullable(UserI.class), any(UserI.class), anyString()); + when(mockSystemHelper.getOrDefault("JH_XNAT_USERNAME", "jupyterhub")).thenReturn("jupyterhub"); + when(mockSystemHelper.getOrDefault("JH_XNAT_PASSWORD", "jupyterhub")).thenReturn("jupyterhub"); + when(mockSystemHelper.getOrDefault("JH_XNAT_SERVICE_USER_ENABLED", "false")).thenReturn("false"); + // Test jupyterHubUserInitializer.callImpl(); @@ -164,6 +183,38 @@ public void test_UserCreated() throws Exception { verify(mockUser).setEnabled(eq(false)); } + @Test + public void test_UserCreatedFromEnv() throws Exception { + // Setup + when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + when(mockXnatAppInfo.isInitialized()).thenReturn(true); + when(mockUserManagementService.exists(username)).thenReturn(false); + UserI mockUser = mock(UserI.class); + when(mockUserManagementService.createUser()).thenReturn(mockUser); + when(mockRoleService.addRole(nullable(UserI.class), any(UserI.class), anyString())).thenReturn(true); + + when(mockSystemHelper.getOrDefault("JH_XNAT_USERNAME", "jupyterhub")).thenReturn("jupyterhub_env"); + when(mockSystemHelper.getOrDefault("JH_XNAT_PASSWORD", "jupyterhub")).thenReturn("jupyterhub_env"); + when(mockSystemHelper.getOrDefault("JH_XNAT_SERVICE_USER_ENABLED", "false")).thenReturn("true"); + + // Test + jupyterHubUserInitializer.callImpl(); + + // Verify + verify(mockUserManagementService, times(1)).save(eq(mockUser), + nullable(UserI.class), // Users.getAdminUser() is null during tests... + anyBoolean(), + any(EventDetails.class)); + verify(mockRoleService, times(1)).addRole(nullable(UserI.class), eq(mockUser), eq("JupyterHub")); + + verify(mockUser).setLogin(eq("jupyterhub_env")); + verify(mockUser).setPassword(eq("jupyterhub_env")); + verify(mockUser).setFirstname(anyString()); + verify(mockUser).setLastname(anyString()); + verify(mockUser).setEmail(anyString()); + verify(mockUser).setEnabled(eq(true)); + } + @Test public void test_TaskName() { final String taskName = jupyterHubUserInitializer.getTaskName(); diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelperTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelperTest.java index 14b945a..cad58f0 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelperTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/impl/DefaultSystemHelperTest.java @@ -21,8 +21,13 @@ public void testNotNull() { } @Test - public void test() { + public void test_get() { systemHelper.getEnv("PATH"); } + @Test + public void test_getOrDefault(){ + systemHelper.getOrDefault("PATH", "default"); + } + } \ No newline at end of file From 4065f3c14f85ce61218abd78f95211ea107bf468 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Fri, 27 Oct 2023 10:52:13 -0500 Subject: [PATCH 40/49] JHP-71: Fixes failing unit tests from XNAT-7903. Unit tests were too strict. --- .../services/impl/DefaultJupyterHubServiceTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java index ac3162e..68657a5 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java @@ -527,6 +527,7 @@ public void testStartServer_CreateUser_Success() throws Exception { public void testStopSever_Failure() throws Exception { // Returning a server should lead to a failure to stop event when(mockJupyterHubClient.getServer(anyString(), anyString())) + .thenReturn(Optional.empty()) .thenReturn(Optional.of(Server.builder().build())); // Test @@ -534,10 +535,10 @@ public void testStopSever_Failure() throws Exception { Thread.sleep(2500); // Async call, need to wait. Is there a better way to test this? // Verify one attempt to stop the sever - verify(mockJupyterHubClient, times(1)).stopServer(username, servername); + verify(mockJupyterHubClient, atLeastOnce()).stopServer(username, servername); // Verify attempts to see if server stopped - verify(mockJupyterHubClient, times(2)).getServer(username, servername); + verify(mockJupyterHubClient, atLeast(2)).getServer(username, servername); // Verify failure to stop event occurred verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); @@ -558,8 +559,8 @@ public void testStopSever_Success() throws Exception { // Verify one attempt to stop the sever verify(mockJupyterHubClient, times(1)).stopServer(username, servername); - // Verify one attempt to see if server stopped - verify(mockJupyterHubClient, times(1)).getServer(username, servername); + // Verify at least one attempt to see if server stopped + verify(mockJupyterHubClient, atLeastOnce()).getServer(username, servername); // Verify stop completed event occurred verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); From a0af017ac940e8226ee8bcc610592e648639a2a1 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Thu, 9 Nov 2023 12:53:50 -0600 Subject: [PATCH 41/49] JHP-72: Fix uncaught exception in unit test --- .../jupyterhub/services/impl/DefaultJupyterHubServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java index 68657a5..d72a355 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java @@ -420,7 +420,7 @@ public void testStartServer_jobTemplateUnavailable() throws Exception { } @Test(timeout = 4000) - public void testStartServer_Timeout() throws UserNotFoundException, ResourceAlreadyExistsException, InterruptedException { + public void testStartServer_Timeout() throws Exception { // Grant permissions when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); From 2a62878d2e3553f8fa82b28c677781fb6c4fe99c Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 8 Jan 2024 10:29:46 -0600 Subject: [PATCH 42/49] JHP-73: Adds dashboarding functionality. --- CHANGELOG.MD | 15 +- .../plugins/jupyterhub/JupyterHubPlugin.java | 1 - .../JupyterUserAuthorization.java | 44 - .../entities/DashboardConfigEntity.java | 169 ++ .../jupyterhub/entities/DashboardEntity.java | 158 ++ .../entities/DashboardFrameworkEntity.java | 76 + .../entities/DashboardScopeEntity.java | 114 ++ .../entities/UserOptionsEntity.java | 11 + .../DashboardFrameworkInitializer.java | 143 ++ .../plugins/jupyterhub/models/Dashboard.java | 23 + .../jupyterhub/models/DashboardConfig.java | 26 + .../jupyterhub/models/DashboardFramework.java | 18 + .../jupyterhub/models/DashboardScope.java | 22 + .../jupyterhub/models/ServerStartRequest.java | 1 + .../jupyterhub/models/XnatUserOptions.java | 1 + .../models/docker/ContainerSpec.java | 1 + .../repositories/DashboardConfigDao.java | 109 ++ .../DashboardFrameworkEntityDao.java | 30 + .../jupyterhub/rest/JupyterHubApi.java | 79 +- .../rest/JupyterHubDashboardConfigsApi.java | 177 ++ .../JupyterHubDashboardFrameworksApi.java | 106 ++ .../DashboardConfigEntityService.java | 11 + .../services/DashboardConfigService.java | 28 + .../DashboardFrameworkEntityService.java | 12 + .../services/DashboardFrameworkService.java | 21 + .../services/DashboardJobTemplateService.java | 14 + .../jupyterhub/services/DashboardService.java | 17 + .../services/UserOptionsService.java | 2 +- ...hboardComputeEnvironmentConfigService.java | 44 + .../impl/DashboardHardwareConfigService.java | 41 + .../impl/DefaultDashboardConfigService.java | 408 +++++ .../DefaultDashboardFrameworkService.java | 155 ++ .../DefaultDashboardJobTemplateService.java | 164 ++ .../impl/DefaultJupyterHubService.java | 63 +- .../impl/DefaultUserOptionsService.java | 22 +- ...HibernateDashboardConfigEntityService.java | 46 + ...ernateDashboardFrameworkEntityService.java | 34 + .../jupyterhub/jupyterhub-dashboards.js | 1292 +++++++++++++++ .../plugin/jupyterhub/jupyterhub-servers.js | 269 ++- .../plugin/jupyterhub/jupyterhub-topnav.js | 41 +- .../templates/screens/topBar/Jupyter.vm | 8 +- .../screens/topBar/Jupyter/Default.vm | 5 +- .../actionsBox/StartJupyterServer.vm | 31 + .../actionsBox/StartJupyterServer.vm | 20 + .../actionsBox/StartJupyterServer.vm | 20 + .../actionsBox/StartJupyterServer.vm | 20 + .../spawner/jupyterhub/project-settings.yaml | 41 + .../spawner/jupyterhub/site-settings.yaml | 64 +- .../JupyterUserAuthorizationTest.java | 103 -- ...shboardFrameworkInitializerTestConfig.java | 26 + ...faultDashboardConfigServiceTestConfig.java | 62 + ...ltDashboardFrameworkServiceTestConfig.java | 20 + ...DashboardJobTemplateServiceTestConfig.java | 32 + .../DefaultJupyterHubServiceConfig.java | 3 + .../DefaultUserOptionsServiceConfig.java | 7 +- .../jupyterhub/config/HibernateConfig.java | 86 + ...boardFrameworkEntityServiceTestConfig.java | 26 + .../config/HibernateEntityServicesConfig.java | 94 ++ .../config/JupyterAuthorizationConfig.java | 20 - .../config/JupyterHubApiConfig.java | 7 +- ...pyterHubDashboardConfigsApiTestConfig.java | 47 + ...erHubDashboardFrameworksApiTestConfig.java | 47 + .../plugins/jupyterhub/config/MockConfig.java | 21 + .../jupyterhub/config/RestApiTestConfig.java | 1 + .../DashboardFrameworkInitializerTest.java | 115 ++ .../jupyterhub/rest/JupyterHubApiTest.java | 106 +- .../JupyterHubDashboardConfigsApiTest.java | 437 +++++ .../JupyterHubDashboardFrameworksApiTest.java | 214 +++ .../DefaultDashboardConfigServiceTest.java | 1442 +++++++++++++++++ .../DefaultDashboardFrameworkServiceTest.java | 238 +++ ...efaultDashboardJobTemplateServiceTest.java | 187 +++ .../impl/DefaultJupyterHubServiceTest.java | 39 +- ...teDashboardFrameworkEntityServiceTest.java | 112 ++ .../jupyterhub/utils/TestingUtils.java | 15 + 74 files changed, 7473 insertions(+), 251 deletions(-) delete mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardConfigEntity.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardEntity.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardFrameworkEntity.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardScopeEntity.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/DashboardFrameworkInitializer.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Dashboard.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardConfig.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardFramework.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardScope.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/DashboardConfigDao.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/DashboardFrameworkEntityDao.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardConfigsApi.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardFrameworksApi.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardConfigEntityService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardConfigService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardFrameworkEntityService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardFrameworkService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardJobTemplateService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DashboardComputeEnvironmentConfigService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DashboardHardwareConfigService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardConfigService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardFrameworkService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardJobTemplateService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardConfigEntityService.java create mode 100644 src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardFrameworkEntityService.java create mode 100644 src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-dashboards.js create mode 100644 src/main/resources/META-INF/resources/templates/screens/xnat_experimentData/actionsBox/StartJupyterServer.vm create mode 100644 src/main/resources/META-INF/xnat/spawner/jupyterhub/project-settings.yaml delete mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DashboardFrameworkInitializerTestConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardConfigServiceTestConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardFrameworkServiceTestConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardJobTemplateServiceTestConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateDashboardFrameworkEntityServiceTestConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateEntityServicesConfig.java delete mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterAuthorizationConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubDashboardConfigsApiTestConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubDashboardFrameworksApiTestConfig.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/DashboardFrameworkInitializerTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardConfigsApiTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardFrameworksApiTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardConfigServiceTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardFrameworkServiceTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardJobTemplateServiceTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardFrameworkEntityServiceTest.java create mode 100644 src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/TestingUtils.java diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 38c3424..8bc8950 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -5,12 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- [JHP-73][]: Adds dashboards functionality to the JupyterHub plugin. May require browser cache to be cleared. + +### Changed + +- Requires XNAT 1.8.10 or higher. + ## [1.0.1] - 2023-10-12 -### Features +### Added + - Added changelog. ### Fixed + - [JHP-68][]: Updates date format patterns in the Server, Token, and User class to handle optional fractions of seconds. - [JHP-69][]: Adds a new preference for JupyterHub Host URL. This preference is used to build the URL for linking users to their Jupyter notebook server. Addresses an issue encountered in multi-node XNAT deployments. @@ -18,3 +30,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [JHP-68]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-68 [JHP-69]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-69 +[JHP-73]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-73 diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java index 93c7adf..acd8f0d 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/JupyterHubPlugin.java @@ -29,7 +29,6 @@ "org.nrg.xnatx.plugins.jupyterhub.listeners", "org.nrg.xnatx.plugins.jupyterhub.repositories", "org.nrg.xnatx.plugins.jupyterhub.utils", - "org.nrg.xnatx.plugins.jupyterhub.authorization", "org.nrg.xnatx.plugins.jupyterhub.initialize"}) @Slf4j public class JupyterHubPlugin { diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java deleted file mode 100644 index af24bbb..0000000 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorization.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.authorization; - -import org.aspectj.lang.JoinPoint; -import org.nrg.xapi.authorization.UserXapiAuthorization; -import org.nrg.xdat.security.helpers.AccessLevel; -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xft.security.UserI; -import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; -import org.springframework.stereotype.Component; - -import javax.servlet.http.HttpServletRequest; - -@Component -public class JupyterUserAuthorization extends UserXapiAuthorization { - - private static final String JUPYTER_ROLE = "Jupyter"; - - private final RoleHolder roleHolder; - private final JupyterHubPreferences jupyterHubPreferences; - - public JupyterUserAuthorization(final RoleHolder roleHolder, - final JupyterHubPreferences jupyterHubPreferences) { - this.roleHolder = roleHolder; - this.jupyterHubPreferences = jupyterHubPreferences; - } - - @Override - protected boolean checkImpl(AccessLevel accessLevel, JoinPoint joinPoint, UserI user, HttpServletRequest request) { - boolean userAuth = super.checkImpl(accessLevel, joinPoint, user, request); - boolean jupyterAuth = checkJupyter(user); - - return userAuth && jupyterAuth; - } - - @Override - protected boolean considerGuests() { - return false; - } - - protected boolean checkJupyter(UserI user) { - return jupyterHubPreferences.getAllUsersCanStartJupyter() || roleHolder.checkRole(user, JUPYTER_ROLE); - } - -} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardConfigEntity.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardConfigEntity.java new file mode 100644 index 0000000..dd0bd19 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardConfigEntity.java @@ -0,0 +1,169 @@ +package org.nrg.xnatx.plugins.jupyterhub.entities; + +import lombok.*; +import org.nrg.framework.constants.Scope; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; +import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.models.ComputeEnvironment; +import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; +import org.nrg.xnat.compute.models.Hardware; +import org.nrg.xnat.compute.models.HardwareConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardScope; + +import javax.persistence.*; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Entity +@Table(name = "xhbm_dashboard_config_entity") +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class DashboardConfigEntity extends AbstractHibernateEntity { + + private DashboardEntity dashboard; + private Map scopes; + private ComputeEnvironmentConfigEntity computeEnvironmentConfig; + private HardwareConfigEntity hardwareConfig; + + @OneToOne(mappedBy = "dashboardConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public DashboardEntity getDashboard() { + return dashboard; + } + + public void setDashboard(final DashboardEntity dashboard) { + this.dashboard = dashboard; + } + + @OneToMany(mappedBy = "dashboardConfig", cascade = CascadeType.ALL, orphanRemoval = true) + public Map getScopes() { + return scopes; + } + + public void setScopes(final Map scopes) { + this.scopes = scopes; + } + + @OneToOne + @JoinColumn(name = "compute_environment_config_id", referencedColumnName = "id") + public ComputeEnvironmentConfigEntity getComputeEnvironmentConfig() { + return computeEnvironmentConfig; + } + + public void setComputeEnvironmentConfig(final ComputeEnvironmentConfigEntity computeEnvironmentConfig) { + this.computeEnvironmentConfig = computeEnvironmentConfig; + } + + @OneToOne + @JoinColumn(name = "hardware_config_id", referencedColumnName = "id") + public HardwareConfigEntity getHardwareConfig() { + return hardwareConfig; + } + + public void setHardwareConfig(final HardwareConfigEntity hardwareConfig) { + this.hardwareConfig = hardwareConfig; + } + + /** + * Creates a new entity from the given pojo. + * @param pojo The pojo from which to create the entity. + * @return The newly created entity. + */ + public static DashboardConfigEntity fromPojo(final DashboardConfig pojo) { + final DashboardConfigEntity entity = new DashboardConfigEntity(); + entity.update(pojo); + return entity; + } + + /** + * Converts the entity to a pojo. The compute environment config and hardware config are converted to pojos + * with only the id and name set. + * @return The pojo representation of the entity. + */ + public DashboardConfig toPojo() { + // Only the id and name is needed for the compute environment config and hardware config + ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder() + .id(getComputeEnvironmentConfig().getId()) + .computeEnvironment(ComputeEnvironment.builder() + .name(getComputeEnvironmentConfig().getComputeEnvironment().getName()) + .build()) + .build(); + + HardwareConfig hardwareConfig = HardwareConfig.builder() + .id(getHardwareConfig().getId()) + .hardware(Hardware.builder() + .name(getHardwareConfig().getHardware().getName()) + .build()) + .build(); + + return DashboardConfig.builder() + .id(getId()) + .dashboard(getDashboard().toPojo()) + .scopes(getScopes().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toPojo()))) + .computeEnvironmentConfig(computeEnvironmentConfig) + .hardwareConfig(hardwareConfig) + .build(); + } + + /** + * Updates the entity with the values from the given pojo. Updating the compute environment config and hardware + * config is not supported. Updating the dashboard config -> dashboard -> dashboard framework relationship is not + * supported and must be done externally. + * @param pojo The pojo from which to update the entity. + */ + public void update(final DashboardConfig pojo) { + if (getDashboard() == null) { + setDashboard(DashboardEntity.fromPojo(pojo.getDashboard())); + } else { + getDashboard().update(pojo.getDashboard()); + } + + getDashboard().setDashboardConfig(this); + + if (getScopes() == null) { + // Create the entity scopes from the pojo scopes + setScopes(pojo.getScopes() + .entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> DashboardScopeEntity.fromPojo(e.getValue())))); + } else { + // Update the existing entity scopes with the pojo scopes + // Collect the existing entity scopes and the updated pojo scopes + Map existingEntityScopes = getScopes(); + Map updatedPojoScopes = pojo.getScopes(); + + // Collect and remove scopes that are no longer in the updated pojo but are in the existing entity + Set scopesToRemove = existingEntityScopes.keySet().stream() + .filter(scope -> !updatedPojoScopes.containsKey(scope)) + .collect(Collectors.toSet()); + + scopesToRemove.forEach(scope -> { + DashboardScopeEntity scopeToRemove = existingEntityScopes.remove(scope); + scopeToRemove.setDashboardConfig(null); + }); + + // Update existing and add new scopes + updatedPojoScopes.forEach((scope, dashboardScope) -> { + DashboardScopeEntity existingScope = existingEntityScopes.get(scope); + if (existingScope != null) { + // Update the existing scope + existingScope.update(dashboardScope); + } else { + // Add new scope + existingEntityScopes.put(scope, DashboardScopeEntity.fromPojo(dashboardScope)); + } + }); + } + + // Set the dashboard config on the scope entities + getScopes().values().forEach(s -> s.setDashboardConfig(this)); + + // Updating the compute environment config and hardware config is not supported + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardEntity.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardEntity.java new file mode 100644 index 0000000..f7e9cc5 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardEntity.java @@ -0,0 +1,158 @@ +package org.nrg.xnatx.plugins.jupyterhub.entities; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; +import org.nrg.xnatx.plugins.jupyterhub.models.Dashboard; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.OneToOne; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.FetchType; + +@Entity +@Table(name = "xhbm_dashboard_entity") +@ToString +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Slf4j +public class DashboardEntity extends AbstractHibernateEntity { + + private String name; + private String description; + private String command; + private String fileSource; + private String gitRepoUrl; + private String gitRepoBranch; + private String mainFilePath; + + @ToString.Exclude @EqualsAndHashCode.Exclude private DashboardConfigEntity dashboardConfig; + @ToString.Exclude @EqualsAndHashCode.Exclude private DashboardFrameworkEntity dashboardFramework; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Column(length = 4096) + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Column(length = 4096) + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public String getFileSource() { + return fileSource; + } + + public void setFileSource(String fileSource) { + this.fileSource = fileSource; + } + + + public String getGitRepoUrl() { + return gitRepoUrl; + } + + public void setGitRepoUrl(String gitRepoUrl) { + this.gitRepoUrl = gitRepoUrl; + } + + public String getGitRepoBranch() { + return gitRepoBranch; + } + + public void setGitRepoBranch(String gitRepoBranch) { + this.gitRepoBranch = gitRepoBranch; + } + + public String getMainFilePath() { + return mainFilePath; + } + + public void setMainFilePath(String gitRepoPath) { + this.mainFilePath = gitRepoPath; + } + + @OneToOne + public DashboardConfigEntity getDashboardConfig() { + return dashboardConfig; + } + + public void setDashboardConfig(final DashboardConfigEntity dashboardConfig) { + this.dashboardConfig = dashboardConfig; + } + + @ManyToOne(fetch = FetchType.EAGER) + public DashboardFrameworkEntity getDashboardFramework() { + return dashboardFramework; + } + + public void setDashboardFramework(final DashboardFrameworkEntity dashboardFramework) { + this.dashboardFramework = dashboardFramework; + } + + /** + * Creates a new entity from the given pojo. + * @param pojo The pojo from which to create the entity. + * @return The newly created entity. + */ + public static DashboardEntity fromPojo(final Dashboard pojo) { + final DashboardEntity entity = new DashboardEntity(); + entity.update(pojo); + return entity; + } + + /** + * Converts the entity to a pojo. + * @return The pojo representation of the entity. + */ + public Dashboard toPojo() { + final String framework = this.getDashboardFramework() != null ? this.getDashboardFramework().getName() : null; + + return Dashboard.builder() + .name(this.getName()) + .description(this.getDescription()) + .framework(framework) + .command(this.getCommand()) + .fileSource(this.getFileSource()) + .gitRepoUrl(this.getGitRepoUrl()) + .gitRepoBranch(this.getGitRepoBranch()) + .mainFilePath(this.getMainFilePath()) + .build(); + } + + /** + * Updates the entity with the values from the given pojo. Doesn't update the ID, that's immutable. + * @param pojo The pojo from which to update the entity. + */ + public void update(final Dashboard pojo) { + // Don't update the ID, that's immutable + // DashboardFrameworkEntity must be set separately + this.setName(pojo.getName()); + this.setDescription(pojo.getDescription()); + this.setCommand(pojo.getCommand()); + this.setFileSource(pojo.getFileSource()); + this.setGitRepoUrl(pojo.getGitRepoUrl()); + this.setGitRepoBranch(pojo.getGitRepoBranch()); + this.setMainFilePath(pojo.getMainFilePath()); + } +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardFrameworkEntity.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardFrameworkEntity.java new file mode 100644 index 0000000..c7d39d7 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardFrameworkEntity.java @@ -0,0 +1,76 @@ +package org.nrg.xnatx.plugins.jupyterhub.entities; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardFramework; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +@Entity +@ToString +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Table(name = "xhbm_dashboard_framework_entity", uniqueConstraints = {@UniqueConstraint(columnNames = {"name"})}) +@Slf4j +public class DashboardFrameworkEntity extends AbstractHibernateEntity { + + private String name; + private String commandTemplate; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Column(length = 4096) + public String getCommandTemplate() { + return commandTemplate; + } + + public void setCommandTemplate(String commandTemplate) { + this.commandTemplate = commandTemplate; + } + + /** + * Creates a new entity from the pojo representation. + * @param pojo The pojo representation of the entity. + * @return The entity representation of the pojo. + */ + public static DashboardFrameworkEntity fromPojo(DashboardFramework pojo) { + final DashboardFrameworkEntity entity = new DashboardFrameworkEntity(); + entity.update(pojo); + return entity; + } + + /** + * Updates the entity with the pojo representation. Does not update the ID. + * @param pojo The pojo representation of the entity. + */ + public void update(DashboardFramework pojo) { + // Don't update the ID, that's immutable + this.setName(pojo.getName()); + this.setCommandTemplate(pojo.getCommandTemplate()); + } + + /** + * Converts the entity to a pojo representation. + * @return The pojo representation of the entity. + */ + public DashboardFramework toPojo() { + return DashboardFramework.builder() + .id(this.getId()) + .name(this.getName()) + .commandTemplate(this.getCommandTemplate()) + .build(); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardScopeEntity.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardScopeEntity.java new file mode 100644 index 0000000..d21f892 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/DashboardScopeEntity.java @@ -0,0 +1,114 @@ +package org.nrg.xnatx.plugins.jupyterhub.entities; + +import lombok.*; +import org.nrg.framework.constants.Scope; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardScope; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "xhbm_dashboard_scope_entity") +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode +public class DashboardScopeEntity { + + private long id; + private String scope; + private boolean enabled; + private Set ids; + + @ToString.Exclude @EqualsAndHashCode.Exclude private DashboardConfigEntity dashboardConfig; + + @Id + @GeneratedValue + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @ElementCollection + public Set getIds() { + if (ids == null) { + ids = new HashSet<>(); + } + + return ids; + } + + public void setIds(Set ids) { + this.ids = ids; + } + + @ManyToOne + public DashboardConfigEntity getDashboardConfig() { + return dashboardConfig; + } + + public void setDashboardConfig(final DashboardConfigEntity dashboardConfig) { + this.dashboardConfig = dashboardConfig; + } + + /** + * Creates a new DashboardScopeEntity from the given pojo. + * @param pojo The pojo to create the entity from + * @return The newly created entity + */ + public static DashboardScopeEntity fromPojo(final DashboardScope pojo) { + final DashboardScopeEntity entity = new DashboardScopeEntity(); + entity.update(pojo); + return entity; + } + + /** + * Updates this entity with the values from the given pojo. + * @param pojo The pojo to update from + */ + public void update(final DashboardScope pojo) { + this.setScope(pojo.getScope().toString()); + this.setEnabled(pojo.isEnabled()); + + // clear ids then add new ids + this.getIds().clear(); + + if (pojo.getIds() != null && !pojo.getIds().isEmpty()) { + this.getIds().addAll(pojo.getIds()); + } + } + + /** + * Converts this entity to a pojo. + * @return The pojo + */ + public DashboardScope toPojo() { + return DashboardScope.builder() + .scope(Scope.valueOf(this.getScope())) + .enabled(this.isEnabled()) + .ids(new HashSet<>(this.getIds())) + .build(); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/UserOptionsEntity.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/UserOptionsEntity.java index b3944d1..277edb3 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/UserOptionsEntity.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/entities/UserOptionsEntity.java @@ -28,6 +28,7 @@ public class UserOptionsEntity extends AbstractHibernateEntity { private String itemId; private String projectId; private String eventTrackingId; + private Long dashboardConfigId; private TaskTemplate taskTemplate; @@ -94,6 +95,14 @@ public void setEventTrackingId(String trackingId) { this.eventTrackingId = trackingId; } + public Long getDashboardConfigId() { + return dashboardConfigId; + } + + public void setDashboardConfigId(Long dashboardConfigId) { + this.dashboardConfigId = dashboardConfigId; + } + public XnatUserOptions toPojo() { return XnatUserOptions.builder() .userId(userId) @@ -102,6 +111,7 @@ public XnatUserOptions toPojo() { .itemId(itemId) .projectId(projectId) .eventTrackingId(eventTrackingId) + .dashboardConfigId(dashboardConfigId) .taskTemplate(taskTemplate) .build(); } @@ -113,6 +123,7 @@ public void update(final UserOptionsEntity update) { this.setItemId(update.getItemId()); this.setProjectId(update.getProjectId()); this.setEventTrackingId(update.getEventTrackingId()); + this.setDashboardConfigId(update.getDashboardConfigId()); this.setTaskTemplate(update.getTaskTemplate()); } } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/DashboardFrameworkInitializer.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/DashboardFrameworkInitializer.java new file mode 100644 index 0000000..a5fdc66 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/initialize/DashboardFrameworkInitializer.java @@ -0,0 +1,143 @@ +package org.nrg.xnatx.plugins.jupyterhub.initialize; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; +import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardFramework; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Initializes the default dashboard frameworks (Panel, Streamlit, Voilà, Dash). + */ +@Component +@Slf4j +public class DashboardFrameworkInitializer extends AbstractInitializingTask { + + private final XFTManagerHelper xftManagerHelper; + private final XnatAppInfo appInfo; + private final DashboardFrameworkService dashboardFrameworkService; + + @Autowired + public DashboardFrameworkInitializer(final XFTManagerHelper xftManagerHelper, + final XnatAppInfo appInfo, + final DashboardFrameworkService dashboardFrameworkService) { + this.xftManagerHelper = xftManagerHelper; + this.appInfo = appInfo; + this.dashboardFrameworkService = dashboardFrameworkService; + } + + /** + * Returns the name of the task. + * @return The name of the task. + */ + @Override + public String getTaskName() { + return "DashboardFrameworkInitializer"; + } + + /** + * Creates the default dashboard frameworks (Panel, Streamlit, Voilà, Dash) if they do not already exist. + * @throws InitializingTaskException When the XFTManagerHelper or XnatAppInfo is not initialized. + */ + @Override + protected void callImpl() throws InitializingTaskException { + log.info("Initializing default dashboard framework."); + + if (!xftManagerHelper.isInitialized() || !appInfo.isInitialized()) { + log.debug("XFTManagerHelper or XnatAppInfo is not initialized, skipping creation."); + throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); + } + + final DashboardFramework panel, streamlit, voila, dash; + + panel = DashboardFramework.builder() + .name("Panel") + .commandTemplate( + "jhsingle-native-proxy " + + "--port 8888 " + + "--destport 5006 " + + "--repo {repo} " + + "--repobranch {repobranch} " + + "--repofolder /home/jovyan/dashboards " + + "bokeh-root-cmd /home/jovyan/dashboards/{mainFilePath} " + + "{--}port={port} " + + "{--}allow-websocket-origin={origin_host} " + + "{--}prefix={base_url} " + + "{--}server=panel" + ).build(); + + streamlit = DashboardFramework.builder() + .name("Streamlit") + .commandTemplate( + "jhsingle-native-proxy " + + "--port 8888 " + + "--destport 8501 " + + "--repo {repo} " + + "--repobranch {repobranch} " + + "--repofolder /home/jovyan/dashboards " + + "streamlit run /home/jovyan/dashboards/{mainFilePath} " + + "{--}server.port {port} " + + "{--}server.headless True " + + "{--}server.fileWatcherType none" + ).build(); + + voila = DashboardFramework.builder() + .name("Voila") + .commandTemplate( + "jhsingle-native-proxy " + + "--port 8888 " + + "--destport 0 " + + "--repo {repo} " + + "--repobranch {repobranch} " + + "--repofolder /home/jovyan/dashboards " + + "voila /home/jovyan/dashboards/{mainFilePath} " + + "{--}port {port} " + + "{--}no-browser " + + "{--}Voila.base_url={base_url}/ " + + "{--}Voila.server_url=/ " + + "{--}Voila.ip=0.0.0.0 " + + "{--}Voila.tornado_settings allow_origin={origin_host} " + + "--progressive" + ).build(); + + dash = DashboardFramework.builder() + .name("Dash") + .commandTemplate( + "jhsingle-native-proxy " + + "--port=8888 " + + "--destport=8050 " + + "--repo={repo} " + + "--repobranch={repobranch} " + + "--repofolder=/home/jovyan/dashboards " + + "plotlydash-tornado-cmd /home/jovyan/dashboards/{mainFilePath} " + + "{--}port={port} " + + "{--}ip 0.0.0.0" + ).build(); + + commitDashboardFramework(panel); + commitDashboardFramework(streamlit); + commitDashboardFramework(voila); + commitDashboardFramework(dash); + } + + /** + * Commits the dashboard framework to the database. + * @param framework The dashboard framework to commit. + */ + protected void commitDashboardFramework(DashboardFramework framework) { + try { + if (!dashboardFrameworkService.get(framework.getName()).isPresent()) { + dashboardFrameworkService.create(framework); + } else { + log.info("Dashboard framework {} already exists, skipping creation.", framework.getName()); + } + } catch (Exception e) { + log.error("Error creating default dashboard framework {}", framework.getName(), e); + } + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Dashboard.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Dashboard.java new file mode 100644 index 0000000..4038345 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/Dashboard.java @@ -0,0 +1,23 @@ +package org.nrg.xnatx.plugins.jupyterhub.models; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Slf4j +public class Dashboard { + + private String name; + private String description; + private String framework; + private String command; + private String fileSource; + private String gitRepoUrl; + private String gitRepoBranch; + private String mainFilePath; + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardConfig.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardConfig.java new file mode 100644 index 0000000..a1fc530 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardConfig.java @@ -0,0 +1,26 @@ +package org.nrg.xnatx.plugins.jupyterhub.models; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.nrg.framework.constants.Scope; +import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; +import org.nrg.xnat.compute.models.HardwareConfig; + +import java.util.Map; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class DashboardConfig { + + @ApiModelProperty(position = 0) private Long id; + @ApiModelProperty(position = 1) private Dashboard dashboard; + @ApiModelProperty(position = 2) private Map scopes; + @ApiModelProperty(position = 3) private ComputeEnvironmentConfig computeEnvironmentConfig; + @ApiModelProperty(position = 4) private HardwareConfig hardwareConfig; + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardFramework.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardFramework.java new file mode 100644 index 0000000..7dc2dc4 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardFramework.java @@ -0,0 +1,18 @@ +package org.nrg.xnatx.plugins.jupyterhub.models; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Slf4j +public class DashboardFramework { + + private Long id; + private String name; + private String commandTemplate; + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardScope.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardScope.java new file mode 100644 index 0000000..19b1979 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/DashboardScope.java @@ -0,0 +1,22 @@ +package org.nrg.xnatx.plugins.jupyterhub.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.nrg.framework.constants.Scope; +import org.nrg.xnat.compute.models.AccessScope; + +import java.util.Set; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class DashboardScope implements AccessScope { + + private Scope scope; + private boolean enabled; // Whether the scope is enabled for all ids or only for the ids in the set + private Set ids; // The set of ids that are enabled for this scope (if isEnabled is false) + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java index 7b50c73..0ba2c3b 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/ServerStartRequest.java @@ -24,5 +24,6 @@ public class ServerStartRequest implements UserOptions { private Long computeEnvironmentConfigId; private Long hardwareConfigId; + private Long dashboardConfigId; // Optional, only used for dashboard servers (not notebooks) } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/XnatUserOptions.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/XnatUserOptions.java index 5372aa7..8421ce5 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/XnatUserOptions.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/XnatUserOptions.java @@ -23,6 +23,7 @@ public class XnatUserOptions implements UserOptions { private String itemLabel; private String projectId; private String eventTrackingId; + private Long dashboardConfigId; @JsonProperty("task_template") private TaskTemplate taskTemplate; diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/ContainerSpec.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/ContainerSpec.java index 2549f61..a7f3377 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/ContainerSpec.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/models/docker/ContainerSpec.java @@ -21,6 +21,7 @@ public class ContainerSpec { private String image; + private String command; private Map env; private Map labels; private List mounts; diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/DashboardConfigDao.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/DashboardConfigDao.java new file mode 100644 index 0000000..ea6adc8 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/DashboardConfigDao.java @@ -0,0 +1,109 @@ +package org.nrg.xnatx.plugins.jupyterhub.repositories; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.Criteria; +import org.hibernate.Hibernate; +import org.hibernate.SessionFactory; +import org.hibernate.criterion.Restrictions; +import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; +import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; +import org.nrg.xnat.compute.repositories.HardwareConfigDao; +import org.nrg.xnatx.plugins.jupyterhub.entities.DashboardConfigEntity; +import org.nrg.xnatx.plugins.jupyterhub.entities.DashboardScopeEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +@Slf4j +public class DashboardConfigDao extends AbstractHibernateDAO { + + private final ComputeEnvironmentConfigDao computeEnvironmentConfigDao; + private final HardwareConfigDao hardwareConfigDao; + + // For testing + public DashboardConfigDao(final SessionFactory sessionFactory, + final ComputeEnvironmentConfigDao computeEnvironmentConfigDao, + final HardwareConfigDao hardwareConfigDao) { + super(sessionFactory); + this.computeEnvironmentConfigDao = computeEnvironmentConfigDao; + this.hardwareConfigDao = hardwareConfigDao; + } + + @Autowired + public DashboardConfigDao(final ComputeEnvironmentConfigDao computeEnvironmentConfigDao, + final HardwareConfigDao hardwareConfigDao) { + this.computeEnvironmentConfigDao = computeEnvironmentConfigDao; + this.hardwareConfigDao = hardwareConfigDao; + } + + /** + * Initializes the entity, loading all collections and proxies. + * @param entity The entity to initialize. + */ + @Override + public void initialize(final DashboardConfigEntity entity) { + if (entity == null) { + return; + } + + super.initialize(entity); + + Hibernate.initialize(entity.getDashboard()); + if (entity.getDashboard() != null) { + Hibernate.initialize(entity.getDashboard().getDashboardFramework()); + } + + Hibernate.initialize(entity.getScopes()); + if (entity.getScopes() != null) { + entity.getScopes().forEach((scope, dashboardScopeEntity) -> { + initialize(dashboardScopeEntity); + }); + } + + if (entity.getComputeEnvironmentConfig() != null) { + computeEnvironmentConfigDao.initialize(entity.getComputeEnvironmentConfig()); + } + + if (entity.getHardwareConfig() != null) { + hardwareConfigDao.initialize(entity.getHardwareConfig()); + } + } + + /** + * Initializes the scope entity, loading all collections and proxies. + * @param entity The scope entity to initialize. + */ + public void initialize(DashboardScopeEntity entity) { + if (entity == null) { + return; + } + + Hibernate.initialize(entity); + Hibernate.initialize(entity.getIds()); + } + + /** + * Checks if the given compute environment config is in use by a dashboard config. + * @param id The ID of the compute environment config to check. + * @return True if the compute environment config is used by a dashboard config, otherwise false. + */ + public boolean isComputeEnvironmentConfigInUse(final long id) { + Criteria criteria = getSession().createCriteria(DashboardConfigEntity.class); + criteria.add(Restrictions.eq("computeEnvironmentConfig.id", id)); + criteria.setMaxResults(1); + return criteria.uniqueResult() != null; + } + + /** + * Checks if the given hardware config is in use by a dashboard config. + * @param id The ID of the hardware config to check. + * @return True if the hardware config is used by a dashboard config, otherwise false. + */ + public boolean isHardwareConfigInUse(final long id) { + Criteria criteria = getSession().createCriteria(DashboardConfigEntity.class); + criteria.add(Restrictions.eq("hardwareConfig.id", id)); + criteria.setMaxResults(1); + return criteria.uniqueResult() != null; + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/DashboardFrameworkEntityDao.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/DashboardFrameworkEntityDao.java new file mode 100644 index 0000000..5fc51ac --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/repositories/DashboardFrameworkEntityDao.java @@ -0,0 +1,30 @@ +package org.nrg.xnatx.plugins.jupyterhub.repositories; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.SessionFactory; +import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; +import org.nrg.xnatx.plugins.jupyterhub.entities.DashboardFrameworkEntity; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@Slf4j +public class DashboardFrameworkEntityDao extends AbstractHibernateDAO { + + // For testing + public DashboardFrameworkEntityDao(final SessionFactory sessionFactory) { + super(sessionFactory); + } + + /** + * Finds a dashboard framework by name. + * @param name The name of the dashboard framework to find. + * @return The dashboard framework if found, otherwise null. + */ + public Optional findFrameworkByName(final String name) { + log.debug("Looking for dashboard framework with name {}", name); + return Optional.ofNullable(this.findByUniqueProperty("name", name)); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java index 0740e99..5af0436 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApi.java @@ -4,8 +4,12 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.nrg.framework.annotations.XapiRestController; +import org.nrg.xapi.exceptions.InsufficientPrivilegesException; import org.nrg.xapi.exceptions.NotFoundException; -import org.nrg.xapi.rest.*; +import org.nrg.xapi.rest.AbstractXapiRestController; +import org.nrg.xapi.rest.AuthorizedRoles; +import org.nrg.xapi.rest.Username; +import org.nrg.xapi.rest.XapiRequestMapping; import org.nrg.xdat.security.helpers.AccessLevel; import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; @@ -14,13 +18,13 @@ import org.nrg.xft.security.UserI; import org.nrg.xnat.tracking.entities.EventTrackingData; import org.nrg.xnat.tracking.services.EventTrackingDataHibernateService; -import org.nrg.xnatx.plugins.jupyterhub.authorization.JupyterUserAuthorization; import org.nrg.xnatx.plugins.jupyterhub.client.models.Hub; import org.nrg.xnatx.plugins.jupyterhub.client.models.Server; import org.nrg.xnatx.plugins.jupyterhub.client.models.Token; import org.nrg.xnatx.plugins.jupyterhub.client.models.User; import org.nrg.xnatx.plugins.jupyterhub.models.ServerStartRequest; import org.nrg.xnatx.plugins.jupyterhub.models.XnatUserOptions; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; import org.nrg.xnatx.plugins.jupyterhub.services.JupyterHubService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.springframework.beans.factory.annotation.Autowired; @@ -33,7 +37,6 @@ import java.util.List; import java.util.Map; -import static org.nrg.xdat.security.helpers.AccessLevel.Authorizer; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.web.bind.annotation.RequestMethod.*; @@ -47,16 +50,24 @@ public class JupyterHubApi extends AbstractXapiRestController { private final JupyterHubService jupyterHubService; private final UserOptionsService jupyterHubUserOptionsService; private final EventTrackingDataHibernateService eventTrackingDataHibernateService; + private final JupyterHubPreferences jupyterHubPreferences; + private final RoleHolder roleHolder; + + public static final String JUPYTER_ROLE = "Jupyter"; @Autowired public JupyterHubApi(final UserManagementServiceI userManagementService, final RoleHolder roleHolder, final JupyterHubService jupyterHubService, - final UserOptionsService jupyterHubUserOptionsService, EventTrackingDataHibernateService eventTrackingDataHibernateService) { + final UserOptionsService jupyterHubUserOptionsService, + final EventTrackingDataHibernateService eventTrackingDataHibernateService, + final JupyterHubPreferences jupyterHubPreferences) { super(userManagementService, roleHolder); + this.roleHolder = roleHolder; this.jupyterHubService = jupyterHubService; this.jupyterHubUserOptionsService = jupyterHubUserOptionsService; this.eventTrackingDataHibernateService = eventTrackingDataHibernateService; + this.jupyterHubPreferences = jupyterHubPreferences; } @ApiOperation(value = "Get the JupyterHub version.", response = Hub.class) @@ -139,11 +150,17 @@ public Server getNamedServer(@ApiParam(value = "username", required = true) @Pat @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized."), @ApiResponse(code = 500, message = "Unexpected error")}) - @XapiRequestMapping(value = "/users/{username}/server", method = POST, restrictTo = Authorizer) - @AuthDelegate(JupyterUserAuthorization.class) - public void startServer(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, // Unused, but required for auth - @RequestBody final ServerStartRequest serverStartRequest) { - jupyterHubService.startServer(getSessionUser(), serverStartRequest); + @XapiRequestMapping(value = "/users/{username}/server", method = POST, restrictTo = AccessLevel.User) + public void startServer(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, + @RequestBody final ServerStartRequest serverStartRequest) throws InsufficientPrivilegesException { + final UserI user = getSessionUser(); + + // Dashboards can be started by any user, Notebooks can only be started by authorized users + if (isNotDashboard(serverStartRequest)) { + checkJupyterAuthorization(user); + } + + jupyterHubService.startServer(user, serverStartRequest); } @@ -154,16 +171,22 @@ public void startServer(@ApiParam(value = "username", required = true) @PathVari @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized."), @ApiResponse(code = 500, message = "Unexpected error")}) - @XapiRequestMapping(value = "/users/{username}/server/{servername}", method = POST, restrictTo = Authorizer) - @AuthDelegate(JupyterUserAuthorization.class) - public void startNamedServer(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, // Unused, but required for auth + @XapiRequestMapping(value = "/users/{username}/server/{servername}", method = POST, restrictTo = AccessLevel.User) + public void startNamedServer(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, @ApiParam(value = "servername", required = true) @PathVariable("servername") final String servername, - @RequestBody final ServerStartRequest serverStartRequest) { + @RequestBody final ServerStartRequest serverStartRequest) throws InsufficientPrivilegesException { if (!StringUtils.equals(servername, serverStartRequest.getServername())) { throw new IllegalArgumentException("Server name in path does not match server name in request body."); } - jupyterHubService.startServer(getSessionUser(), serverStartRequest); + final UserI user = getSessionUser(); + + // Dashboards can be started by any user, Notebooks can only be started by authorized users + if (isNotDashboard(serverStartRequest)) { + checkJupyterAuthorization(user); + } + + jupyterHubService.startServer(user, serverStartRequest); } @ApiOperation(value = "Returns the last known user options for the default server", response = XnatUserOptions.class) @@ -243,8 +266,7 @@ public void stopServer(@ApiParam(value = "username", required = true) @PathVaria @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized."), @ApiResponse(code = 500, message = "Unexpected error")}) - @AuthDelegate(JupyterUserAuthorization.class) - @XapiRequestMapping(value = "/users/{username}/tokens", method = POST, restrictTo = Authorizer) + @XapiRequestMapping(value = "/users/{username}/tokens", method = POST, restrictTo = AccessLevel.User) public Token createToken(@ApiParam(value = "username", required = true) @PathVariable("username") @Username final String username, @ApiParam(value = "note", required = true) @RequestParam("note") final String note, @ApiParam(value = "expiresIn", required = true) @RequestParam(value = "expiresIn") final Integer expiresIn) throws UserNotFoundException, UserInitException { @@ -255,4 +277,29 @@ public Token createToken(@ApiParam(value = "username", required = true) @PathVar private UserI getUserI(final String username) throws UserNotFoundException, UserInitException { return getUserManagementService().getUser(username); } + + /** + * Checks if the server start request is for starting a dashboard or a notebook. + * @param serverStartRequest The server start request to check. + * @return True if the server start request is for a dashboard, false otherwise. + */ + protected boolean isNotDashboard(ServerStartRequest serverStartRequest) { + return serverStartRequest.getDashboardConfigId() == null; + } + + /** + * Checks if the user is authorized to start a Jupyter notebook server (not a dashboard). + * @param user The user to check. + * @throws InsufficientPrivilegesException If the user is not authorized to start a Jupyter notebook server. + */ + protected void checkJupyterAuthorization(UserI user) throws InsufficientPrivilegesException { + final boolean allUsersCanStartJupyter = jupyterHubPreferences.getAllUsersCanStartJupyter(); + final boolean userHasJupyterRole = roleHolder.checkRole(user, JUPYTER_ROLE); + + // If all users can't start jupyter and the user doesn't have the jupyter role, then they are not authorized + if (!allUsersCanStartJupyter && !userHasJupyterRole) { + throw new InsufficientPrivilegesException(user.getUsername()); + } + } + } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardConfigsApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardConfigsApi.java new file mode 100644 index 0000000..42504a1 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardConfigsApi.java @@ -0,0 +1,177 @@ +package org.nrg.xnatx.plugins.jupyterhub.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.annotations.XapiRestController; +import org.nrg.framework.constants.Scope; +import org.nrg.xapi.exceptions.NotFoundException; +import org.nrg.xapi.rest.AbstractXapiRestController; +import org.nrg.xapi.rest.Project; +import org.nrg.xapi.rest.XapiRequestMapping; +import org.nrg.xdat.security.helpers.AccessLevel; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardConfig; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; +import java.util.stream.Collectors; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@Api("JupyterHub Dashboard Configs API") +@XapiRestController +@RequestMapping("/jupyterhub/dashboards/configs") +@Slf4j +public class JupyterHubDashboardConfigsApi extends AbstractXapiRestController { + + private final DashboardConfigService dashboardConfigService; + + @Autowired + public JupyterHubDashboardConfigsApi(final UserManagementServiceI userManagementService, + final RoleHolder roleHolder, + final DashboardConfigService dashboardConfigService) { + super(userManagementService, roleHolder); + this.dashboardConfigService = dashboardConfigService; + } + + @ApiOperation(value = "Get all dashboard configs.", response = DashboardConfig.class, responseContainer = "List") + @ApiResponses({ + @ApiResponse(code = 200, message = "Successfully retrieved the list of dashboard configs"), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = AccessLevel.Authenticated) + public Iterable getDashboardConfigs() { + return dashboardConfigService.getAll(); + } + + @ApiOperation(value = "Get a dashboard config.", response = DashboardConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Successfully retrieved the dashboard config"), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Dashboard config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = AccessLevel.Authenticated) + public DashboardConfig getDashboardConfig(@PathVariable("id") final Long id) throws NotFoundException { + return dashboardConfigService.retrieve(id).orElseThrow(() -> new NotFoundException("Dashboard config not found.")); + } + + @ApiOperation(value = "Create a dashboard config.", response = DashboardConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Successfully created or updated the dashboard config"), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.CREATED) + @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = RequestMethod.POST, restrictTo = AccessLevel.Admin) + public DashboardConfig createDashboardConfig(@RequestBody final DashboardConfig dashboardConfig) { + return dashboardConfigService.create(dashboardConfig); + } + + @ApiOperation(value = "Update a dashboard config.", response = DashboardConfig.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "Successfully updated the dashboard config"), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 404, message = "Dashboard config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{id}", produces = APPLICATION_JSON_VALUE, method = RequestMethod.PUT, restrictTo = AccessLevel.Admin) + public DashboardConfig updateDashboardConfig(@PathVariable("id") final Long id, + @RequestBody final DashboardConfig dashboardConfig) throws NotFoundException { + if (!dashboardConfig.getId().equals(id)) { + throw new IllegalArgumentException("Dashboard config id does not match id in request path."); + } + + return dashboardConfigService.update(dashboardConfig); + } + + @ApiOperation(value = "Delete a dashboard config.") + @ApiResponses({ + @ApiResponse(code = 204, message = "Dashboard config successfully deleted."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 404, message = "Dashboard config not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + @XapiRequestMapping(value = "/{id}", method = RequestMethod.DELETE, restrictTo = AccessLevel.Admin) + public void deleteDashboardConfig(@PathVariable("id") final Long id) throws NotFoundException { + dashboardConfigService.delete(id); + } + + @ApiOperation(value = "Get all available dashboard configs.", response = DashboardConfig.class, responseContainer = "List") + @ApiResponses({ + @ApiResponse(code = 200, message = "Successfully retrieved the list of available dashboard configs"), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/available", produces = APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = AccessLevel.Authenticated) + public Iterable getAvailableDashboardConfigs(@RequestParam final Map scopes) { + Map executionScope = scopes.entrySet().stream() + .filter(entry -> Scope.getCodes().contains(entry.getKey())) + .collect(Collectors.toMap(entry -> Scope.getScope(entry.getKey()), Map.Entry::getValue)); + + return dashboardConfigService.getAvailable(executionScope); + } + + @ApiOperation(value = "Enable a dashboard config at the site level.") + @ApiResponses({ + @ApiResponse(code = 204, message = "Successfully enabled the dashboard config at the site level."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + @XapiRequestMapping(value = "/{id}/scope/site", method = RequestMethod.POST, restrictTo = AccessLevel.Admin) + public void enableDashboardConfigAtSite(@PathVariable("id") final Long id) throws NotFoundException { + dashboardConfigService.enableForSite(id); + } + + @ApiOperation(value = "Disable a dashboard config at the site level.") + @ApiResponses({ + @ApiResponse(code = 204, message = "Successfully disabled the dashboard config at the site level."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + @XapiRequestMapping(value = "/{id}/scope/site", method = RequestMethod.DELETE, restrictTo = AccessLevel.Admin) + public void disableDashboardConfigAtSite(@PathVariable("id") final Long id) throws NotFoundException { + dashboardConfigService.disableForSite(id); + } + + @ApiOperation(value = "Enable a dashboard config at the project level.") + @ApiResponses({ + @ApiResponse(code = 204, message = "Successfully enabled the dashboard config at the project level."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + @XapiRequestMapping(value = "/{id}/scope/project/{projectId}", method = RequestMethod.POST, restrictTo = AccessLevel.Edit) + public void enableDashboardConfigAtProject(@PathVariable("id") final Long id, + @PathVariable("projectId") @Project final String projectId) throws NotFoundException { + dashboardConfigService.enableForProject(id, projectId); + } + + @ApiOperation(value = "Disable a dashboard config at the project level.") + @ApiResponses({ + @ApiResponse(code = 204, message = "Successfully disabled the dashboard config at the project level."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + @XapiRequestMapping(value = "/{id}/scope/project/{projectId}", method = RequestMethod.DELETE, restrictTo = AccessLevel.Edit) + public void disableDashboardConfigAtProject(@PathVariable("id") final Long id, + @PathVariable("projectId") @Project final String projectId) throws NotFoundException { + dashboardConfigService.disableForProject(id, projectId); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardFrameworksApi.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardFrameworksApi.java new file mode 100644 index 0000000..fb341d9 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardFrameworksApi.java @@ -0,0 +1,106 @@ +package org.nrg.xnatx.plugins.jupyterhub.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.annotations.XapiRestController; +import org.nrg.xapi.exceptions.NotFoundException; +import org.nrg.xapi.rest.AbstractXapiRestController; +import org.nrg.xapi.rest.XapiRequestMapping; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardFramework; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import static org.nrg.xdat.security.helpers.AccessLevel.Admin; +import static org.nrg.xdat.security.helpers.AccessLevel.Authenticated; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.web.bind.annotation.RequestMethod.*; + +@Api("JupyterHub Dashboard Frameworks API") +@XapiRestController +@RequestMapping("/jupyterhub/dashboards/frameworks") +@Slf4j +public class JupyterHubDashboardFrameworksApi extends AbstractXapiRestController { + + private final DashboardFrameworkService dashboardFrameworkService; + + @Autowired + public JupyterHubDashboardFrameworksApi(final UserManagementServiceI userManagementService, + final RoleHolder roleHolder, + final DashboardFrameworkService dashboardFrameworkService) { + super(userManagementService, roleHolder); + this.dashboardFrameworkService = dashboardFrameworkService; + } + + @ApiOperation(value = "Get all dashboard frameworks.") + @ApiResponses({ + @ApiResponse(code = 200, message = "Successfully retrieved the list of dashboard frameworks"), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Authenticated) + public Iterable getAll() { + return dashboardFrameworkService.getAll(); + } + + @ApiOperation(value = "Get a dashboard framework by name.") + @ApiResponses({ + @ApiResponse(code = 200, message = "Successfully retrieved the dashboard framework"), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Dashboard framework not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{name}", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Authenticated) + public DashboardFramework get(@PathVariable("name") String name) throws NotFoundException { + return dashboardFrameworkService.get(name) + .orElseThrow(() -> new NotFoundException("DashboardFramework", name)); + } + + @ApiOperation(value = "Create a dashboard framework.") + @ApiResponses({ + @ApiResponse(code = 200, message = "Successfully created the dashboard framework"), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = POST, restrictTo = Admin) + public DashboardFramework create(@RequestBody DashboardFramework dashboardFramework) { + return dashboardFrameworkService.create(dashboardFramework); + } + + @ApiOperation(value = "Update a dashboard framework.") + @ApiResponses({ + @ApiResponse(code = 200, message = "Successfully updated the dashboard framework"), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Dashboard framework not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{name}", produces = APPLICATION_JSON_VALUE, method = PUT, restrictTo = Admin) + public DashboardFramework update(@RequestBody DashboardFramework dashboardFramework) throws NotFoundException { + return dashboardFrameworkService.update(dashboardFramework); + } + + @ApiOperation(value = "Delete a dashboard framework.") + @ApiResponses({ + @ApiResponse(code = 200, message = "Successfully deleted the dashboard framework"), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized."), + @ApiResponse(code = 404, message = "Dashboard framework not found."), + @ApiResponse(code = 500, message = "Unexpected error") + }) + @XapiRequestMapping(value = "/{name}", produces = APPLICATION_JSON_VALUE, method = DELETE, restrictTo = Admin) + public void delete(@PathVariable("name") String name) throws NotFoundException { + dashboardFrameworkService.delete(name); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardConfigEntityService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardConfigEntityService.java new file mode 100644 index 0000000..f554a71 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardConfigEntityService.java @@ -0,0 +1,11 @@ +package org.nrg.xnatx.plugins.jupyterhub.services; + +import org.nrg.framework.orm.hibernate.BaseHibernateService; +import org.nrg.xnatx.plugins.jupyterhub.entities.DashboardConfigEntity; + +public interface DashboardConfigEntityService extends BaseHibernateService { + + boolean isComputeEnvironmentConfigInUse(Long id); + boolean isHardwareConfigInUse(Long id); + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardConfigService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardConfigService.java new file mode 100644 index 0000000..3125ce9 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardConfigService.java @@ -0,0 +1,28 @@ +package org.nrg.xnatx.plugins.jupyterhub.services; + +import org.nrg.framework.constants.Scope; +import org.nrg.xapi.exceptions.NotFoundException; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardConfig; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public interface DashboardConfigService { + + boolean exists(Long id); + Optional retrieve(Long id); + List getAll(); + DashboardConfig create(DashboardConfig dashboardConfig); + DashboardConfig update(DashboardConfig dashboardConfig) throws NotFoundException; + void delete(Long id) throws NotFoundException; + boolean isAvailable(Long id, Map executionScope); + List getAvailable(Map executionScope); + boolean isValid(DashboardConfig dashboardConfig); + + void enableForSite(Long id) throws NotFoundException; + void disableForSite(Long id) throws NotFoundException; + void enableForProject(Long id, String projectId) throws NotFoundException; + void disableForProject(Long id, String projectId) throws NotFoundException; + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardFrameworkEntityService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardFrameworkEntityService.java new file mode 100644 index 0000000..08e1c1e --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardFrameworkEntityService.java @@ -0,0 +1,12 @@ +package org.nrg.xnatx.plugins.jupyterhub.services; + +import org.nrg.framework.orm.hibernate.BaseHibernateService; +import org.nrg.xnatx.plugins.jupyterhub.entities.DashboardFrameworkEntity; + +import java.util.Optional; + +public interface DashboardFrameworkEntityService extends BaseHibernateService { + + Optional findFrameworkByName(String name); + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardFrameworkService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardFrameworkService.java new file mode 100644 index 0000000..25b5b96 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardFrameworkService.java @@ -0,0 +1,21 @@ +package org.nrg.xnatx.plugins.jupyterhub.services; + +import org.nrg.xapi.exceptions.NotFoundException; +import org.nrg.xnatx.plugins.jupyterhub.models.Dashboard; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardFramework; + +import java.util.List; +import java.util.Optional; + +public interface DashboardFrameworkService { + + DashboardFramework create(DashboardFramework framework); + DashboardFramework update(DashboardFramework framework) throws NotFoundException; + Optional get(Long id); + Optional get(String name); + List getAll(); + void delete(Long id); + void delete(String name); + String resolveCommand(Dashboard dashboard); + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardJobTemplateService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardJobTemplateService.java new file mode 100644 index 0000000..c0e4e19 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardJobTemplateService.java @@ -0,0 +1,14 @@ +package org.nrg.xnatx.plugins.jupyterhub.services; + +import org.nrg.framework.constants.Scope; +import org.nrg.xnat.compute.models.JobTemplate; +import org.nrg.xnat.compute.services.JobTemplateService; + +import java.util.Map; + +public interface DashboardJobTemplateService extends JobTemplateService { + + boolean isAvailable(Long dashboardConfigId, Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope); + JobTemplate resolve(Long dashboardConfigId, Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope); + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardService.java new file mode 100644 index 0000000..fff075a --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/DashboardService.java @@ -0,0 +1,17 @@ +package org.nrg.xnatx.plugins.jupyterhub.services; + +import org.nrg.xapi.exceptions.NotFoundException; +import org.nrg.xnatx.plugins.jupyterhub.models.Dashboard; + +import java.util.List; +import java.util.Optional; + +public interface DashboardService { + + Dashboard create(Dashboard dashboard); + Dashboard update(Dashboard dashboard) throws NotFoundException; + Optional get(Long id); + List getAll(); + void delete(Long id); + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java index ccd8003..c0408dc 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/UserOptionsService.java @@ -26,6 +26,6 @@ public interface UserOptionsService { Optional retrieveUserOptions(UserI user); Optional retrieveUserOptions(UserI user, String servername); - void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, Long computeEnvironmentConfigId, Long hardwareConfigId, String eventTrackingId) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException; + void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, Long computeEnvironmentConfigId, Long hardwareConfigId, Long dashboardConfigId, String eventTrackingId) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException; } diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DashboardComputeEnvironmentConfigService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DashboardComputeEnvironmentConfigService.java new file mode 100644 index 0000000..b036d5f --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DashboardComputeEnvironmentConfigService.java @@ -0,0 +1,44 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.services.impl.DefaultComputeEnvironmentConfigService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +@Service +@Primary +@Slf4j +public class DashboardComputeEnvironmentConfigService extends DefaultComputeEnvironmentConfigService { + + private final DashboardConfigEntityService dashboardConfigEntityService; + + @Autowired + public DashboardComputeEnvironmentConfigService(final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService, + final HardwareConfigEntityService hardwareConfigEntityService, + final DashboardConfigEntityService dashboardConfigEntityService) { + super(computeEnvironmentConfigEntityService, hardwareConfigEntityService); + this.dashboardConfigEntityService = dashboardConfigEntityService; + } + + /** + * Don't allow the user to delete a ComputeEnvironmentConfig if it is in use by a DashboardConfig. + * @param id The ID of the ComputeEnvironmentConfig to delete. + * @throws RuntimeException if the ComputeEnvironmentConfig is in use by a DashboardConfig. + * @throws NotFoundException if the ComputeEnvironmentConfig does not exist. + */ + @Override + public void delete(Long id) throws NotFoundException { + if (dashboardConfigEntityService.isComputeEnvironmentConfigInUse(id)) { + log.error("Cannot delete ComputeEnvironmentConfig with ID {} because it is in use by a DashboardConfig.", id); + throw new RuntimeException("Cannot delete ComputeEnvironmentConfig because it is in use by a DashboardConfig."); + } + + super.delete(id); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DashboardHardwareConfigService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DashboardHardwareConfigService.java new file mode 100644 index 0000000..735470c --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DashboardHardwareConfigService.java @@ -0,0 +1,41 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.services.impl.DefaultHardwareConfigService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigEntityService; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +@Service +@Primary +@Slf4j +public class DashboardHardwareConfigService extends DefaultHardwareConfigService { + + private final DashboardConfigEntityService dashboardConfigEntityService; + + public DashboardHardwareConfigService(final HardwareConfigEntityService hardwareConfigEntityService, + final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService, + final DashboardConfigEntityService dashboardConfigEntityService) { + super(hardwareConfigEntityService, computeEnvironmentConfigEntityService); + this.dashboardConfigEntityService = dashboardConfigEntityService; + } + + /** + * Don't allow the user to delete a HardwareConfig if it is in use by a DashboardConfig. + * @param id The ID of the HardwareConfig to delete. + * @throws RuntimeException if the HardwareConfig is in use by a DashboardConfig. + * @throws NotFoundException if the HardwareConfig does not exist. + */ + @Override + public void delete(Long id) throws NotFoundException { + if (dashboardConfigEntityService.isHardwareConfigInUse(id)) { + log.error("Cannot delete HardwareConfig with ID {} because it is in use by a DashboardConfig.", id); + throw new RuntimeException("Cannot delete HardwareConfig because it is in use by a DashboardConfig."); + } + + super.delete(id); + } +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardConfigService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardConfigService.java new file mode 100644 index 0000000..d71eb6e --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardConfigService.java @@ -0,0 +1,408 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.nrg.framework.constants.Scope; +import org.nrg.xapi.exceptions.NotFoundException; +import org.nrg.xnat.compute.entities.ComputeEnvironmentConfigEntity; +import org.nrg.xnat.compute.entities.HardwareConfigEntity; +import org.nrg.xnat.compute.models.AccessScope; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.nrg.xnatx.plugins.jupyterhub.entities.DashboardConfigEntity; +import org.nrg.xnatx.plugins.jupyterhub.entities.DashboardFrameworkEntity; +import org.nrg.xnatx.plugins.jupyterhub.models.Dashboard; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardScope; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class DefaultDashboardConfigService implements DashboardConfigService { + + private final DashboardConfigEntityService dashboardConfigEntityService; + private final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService; + private final HardwareConfigEntityService hardwareConfigEntityService; + private final DashboardFrameworkEntityService dashboardFrameworkEntityService; + + @Autowired + public DefaultDashboardConfigService(final DashboardConfigEntityService dashboardConfigEntityService, + final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService, + final HardwareConfigEntityService hardwareConfigEntityService, + final DashboardFrameworkEntityService dashboardFrameworkEntityService) { + super(); + this.dashboardConfigEntityService = dashboardConfigEntityService; + this.computeEnvironmentConfigEntityService = computeEnvironmentConfigEntityService; + this.hardwareConfigEntityService = hardwareConfigEntityService; + this.dashboardFrameworkEntityService = dashboardFrameworkEntityService; + } + + /** + * Checks if a dashboard config exists with the given id + * @param id The dashboard config id to check for + * @return True if a dashboard config exists with the given id, false otherwise + */ + @Override + public boolean exists(Long id) { + return dashboardConfigEntityService.exists("id", id); + } + + /** + * Retrieves a dashboard config with the given id + * @param id The id of the dashboard config to retrieve + * @return The dashboard config with the given id, if found + */ + @Override + public Optional retrieve(Long id) { + DashboardConfigEntity entity = dashboardConfigEntityService.retrieve(id); + return Optional.ofNullable(entity).map(DashboardConfigEntity::toPojo); + } + + /** + * Retrieves all dashboard configs + * @return A list of all dashboard configs + */ + @Override + public List getAll() { + return dashboardConfigEntityService.getAll().stream() + .map(DashboardConfigEntity::toPojo) + .collect(Collectors.toList()); + } + + /** + * Creates a new dashboard config + * @param dashboardConfig The dashboard config to create + * @return The created dashboard config + */ + @Override + public DashboardConfig create(DashboardConfig dashboardConfig) { + validate(dashboardConfig); + + // Create the dashboard config + DashboardConfigEntity dashboardConfigEntity = dashboardConfigEntityService.create(DashboardConfigEntity.fromPojo(dashboardConfig)); + + // Then add the compute environment config and hardware config + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity = computeEnvironmentConfigEntityService.retrieve(dashboardConfig.getComputeEnvironmentConfig().getId()); + HardwareConfigEntity hardwareConfigEntity = hardwareConfigEntityService.retrieve(dashboardConfig.getHardwareConfig().getId()); + + // Then add the dashboard framework if it's not custom + if (StringUtils.isNotBlank(dashboardConfig.getDashboard().getFramework()) && + !dashboardConfig.getDashboard().getFramework().equalsIgnoreCase("custom")) { + DashboardFrameworkEntity dashboardFrameworkEntity = dashboardFrameworkEntityService.findFrameworkByName(dashboardConfig.getDashboard().getFramework()) + .orElseThrow(() -> new IllegalArgumentException("Dashboard framework does not exist")); + dashboardConfigEntity.getDashboard().setDashboardFramework(dashboardFrameworkEntity); + } else { + dashboardConfigEntity.getDashboard().setDashboardFramework(null); + } + + if (computeEnvironmentConfigEntity == null) { + throw new IllegalArgumentException("Compute environment config does not exist"); + } + + if (hardwareConfigEntity == null) { + throw new IllegalArgumentException("Hardware config does not exist"); + } + + dashboardConfigEntity.setComputeEnvironmentConfig(computeEnvironmentConfigEntity); + dashboardConfigEntity.setHardwareConfig(hardwareConfigEntity); + + dashboardConfigEntityService.update(dashboardConfigEntity); + + // Then return the dashboard config + return dashboardConfigEntityService.retrieve(dashboardConfigEntity.getId()).toPojo(); + } + + /** + * Updates a dashboard config + * @param dashboardConfig The dashboard config to update + * @return The updated dashboard config + * @throws NotFoundException If the dashboard config does not exist + */ + @Override + public DashboardConfig update(DashboardConfig dashboardConfig) throws NotFoundException { + // Validate + if (dashboardConfig == null) { + // Can't check ID if null + throw new IllegalArgumentException("Dashboard config cannot be null"); + } else if (dashboardConfig.getId() == null) { + // ID is required for update, but not create + throw new IllegalArgumentException("Dashboard config id cannot be null"); + } else if (!exists(dashboardConfig.getId())) { + // Can't update a dashboard config that doesn't exist + throw new NotFoundException("Dashboard config not found"); + } + + validate(dashboardConfig); + + // Update the dashboard config + DashboardConfigEntity entity = dashboardConfigEntityService.retrieve(dashboardConfig.getId()); + entity.update(dashboardConfig); + + // Update the compute environment config + ComputeEnvironmentConfigEntity computeEnvironmentConfigEntity = computeEnvironmentConfigEntityService.retrieve(dashboardConfig.getComputeEnvironmentConfig().getId()); + entity.setComputeEnvironmentConfig(computeEnvironmentConfigEntity); + + // Update the hardware config + HardwareConfigEntity hardwareConfigEntity = hardwareConfigEntityService.retrieve(dashboardConfig.getHardwareConfig().getId()); + entity.setHardwareConfig(hardwareConfigEntity); + + // Then add the dashboard framework if it's not custom + if (StringUtils.isNotBlank(dashboardConfig.getDashboard().getFramework()) && + !dashboardConfig.getDashboard().getFramework().equalsIgnoreCase("custom")) { + DashboardFrameworkEntity dashboardFrameworkEntity = dashboardFrameworkEntityService.findFrameworkByName(dashboardConfig.getDashboard().getFramework()) + .orElseThrow(() -> new IllegalArgumentException("Dashboard framework does not exist")); + entity.getDashboard().setDashboardFramework(dashboardFrameworkEntity); + } else { + entity.getDashboard().setDashboardFramework(null); + } + + // Save the dashboard config + dashboardConfigEntityService.update(entity); + + // Return the updated dashboard config + return dashboardConfigEntityService.retrieve(entity.getId()).toPojo(); + } + + /** + * Deletes a dashboard config + * @param id The id of the dashboard config to delete + * @throws NotFoundException If the dashboard config does not exist + */ + @Override + public void delete(Long id) throws NotFoundException { + if (!exists(id)) { + throw new NotFoundException("DashboardConfigEntity", id); + } + + dashboardConfigEntityService.delete(id); + } + + /** + * Checks if a dashboard config is available + * @param id The id of the dashboard config to check + * @param executionScope The execution scope to check + * @return True if available, false otherwise + */ + @Override + public boolean isAvailable(Long id, Map executionScope) { + final Optional dashboardConfig = retrieve(id); + + if (!dashboardConfig.isPresent()) { + return false; + } + + Map requiredScopes = dashboardConfig.get().getScopes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + return AccessScope.isEnabledFor(requiredScopes, executionScope); + } + + /** + * Gets all available dashboard configs for the given execution scope + * @param executionScope The execution scope to check + * @return A list of all available dashboard configs + */ + @Override + public List getAvailable(Map executionScope) { + return getAll().stream() + .filter(dashboardConfig -> isAvailable(dashboardConfig.getId(), executionScope)) + .collect(Collectors.toList()); + } + + /** + * Checks if a dashboard config is valid + * @param config The dashboard config to check + * @return True if valid, false otherwise + */ + @Override + public boolean isValid(DashboardConfig config) { + try { + validate(config); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + /** + * Enables a dashboard config for the site + * @param id The id of the dashboard config to enable + * @throws NotFoundException If the dashboard config does not exist + */ + @Override + public void enableForSite(Long id) throws NotFoundException { + // Get the dashboard config + final Optional dashboardConfig = retrieve(id); + + // Make sure it exists + if (!dashboardConfig.isPresent()) { + throw new IllegalArgumentException("Dashboard config does not exist"); + } + + // Enable the site scope + final DashboardConfig config = dashboardConfig.get(); + config.getScopes().get(Scope.Site).setEnabled(true); + + update(config); + } + + /** + * Disables a dashboard config for the site + * @param id The id of the dashboard config to disable + * @throws NotFoundException If the dashboard config does not exist + */ + @Override + public void disableForSite(Long id) throws NotFoundException { + // Get the dashboard config + final Optional dashboardConfig = retrieve(id); + + // Make sure it exists + if (!dashboardConfig.isPresent()) { + throw new IllegalArgumentException("Dashboard config does not exist"); + } + + // Disable the site scope + final DashboardConfig config = dashboardConfig.get(); + config.getScopes().get(Scope.Site).setEnabled(false); + + update(config); + } + + /** + * Enables a dashboard config for the specified project + * @param id The id of the dashboard config to enable + * @param projectId The id of the project to enable the dashboard config for + * @throws NotFoundException If the dashboard config does not exist + */ + @Override + public void enableForProject(Long id, String projectId) throws NotFoundException { + // Get the dashboard config + final Optional dashboardConfig = retrieve(id); + + // Make sure it exists + if (!dashboardConfig.isPresent()) { + throw new IllegalArgumentException("Dashboard config does not exist"); + } + + // Enable for the project + final DashboardConfig config = dashboardConfig.get(); + config.getScopes().get(Scope.Project).getIds().add(projectId); + + update(config); + } + + /** + * Disables a dashboard config for the specified project + * @param id The id of the dashboard config to disable + * @param projectId The id of the project to disable the dashboard config for + * @throws NotFoundException If the dashboard config does not exist + */ + @Override + public void disableForProject(Long id, String projectId) throws NotFoundException { + // Get the dashboard config + final Optional dashboardConfig = retrieve(id); + + // Make sure it exists + if (!dashboardConfig.isPresent()) { + throw new IllegalArgumentException("Dashboard config does not exist"); + } + + // Disable for the project + final DashboardConfig config = dashboardConfig.get(); + config.getScopes().get(Scope.Project).getIds().remove(projectId); + + update(config); + } + + /** + * Validates a dashboard config + * @param config The dashboard config to validate + * @throws IllegalArgumentException If invalid. The exception message will contain the validation errors. + */ + protected void validate(final DashboardConfig config) throws IllegalArgumentException { + if (config == null) { + throw new IllegalArgumentException("Dashboard config cannot be null"); + } + + List errors = new ArrayList<>(); + + errors.addAll(validate(config.getDashboard())); + errors.addAll(validate(config.getScopes())); + + if (config.getComputeEnvironmentConfig() == null) { + errors.add("Compute environment config cannot be null"); + } + + if (config.getHardwareConfig() == null) { + errors.add("Hardware config cannot be null"); + } + + if (!errors.isEmpty()) { + throw new IllegalArgumentException("Dashboard config is invalid: " + String.join(", ", errors)); + } + } + + /** + * Validates a dashboard + * @param dashboard The dashboard to validate + * @return A list of errors, if any + */ + protected List validate(final Dashboard dashboard) { + List errors = new ArrayList<>(); + + if (dashboard == null) { + errors.add("Dashboard cannot be null"); + return errors; + } + + if (StringUtils.isBlank(dashboard.getName())) { + errors.add("Dashboard name cannot be blank"); + } + + return errors; + } + + /** + * Validates dashboard scopes + * @param scopes The dashboard scopes to validate + * Must contain a site, project, and data type scope + * @return A list of errors, if any + */ + protected List validate(final Map scopes) { + List errors = new ArrayList<>(); + + if (scopes == null) { + errors.add("Dashboard scopes cannot be null"); + return errors; + } else if (scopes.isEmpty()) { + errors.add("Dashboard scopes cannot be empty"); + return errors; + } + + if (!scopes.containsKey(Scope.Site)) { + errors.add("Dashboard scopes must contain a site scope"); + } + + if (!scopes.containsKey(Scope.Project)) { + errors.add("Dashboard scopes must contain a project scope"); + } + + if (!scopes.containsKey(Scope.DataType)) { + errors.add("Dashboard scopes must contain a data type scope"); + } + + return errors; + } +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardFrameworkService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardFrameworkService.java new file mode 100644 index 0000000..f695524 --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardFrameworkService.java @@ -0,0 +1,155 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.nrg.xapi.exceptions.NotFoundException; +import org.nrg.xnatx.plugins.jupyterhub.entities.DashboardFrameworkEntity; +import org.nrg.xnatx.plugins.jupyterhub.models.Dashboard; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardFramework; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +/** + * Default implementation of the dashboard framework service. This service is responsible for creating, updating, + * retrieving, and deleting dashboard frameworks. + */ +@Service +@Slf4j +public class DefaultDashboardFrameworkService implements DashboardFrameworkService { + + private final DashboardFrameworkEntityService dashboardFrameworkEntityService; + + @Autowired + public DefaultDashboardFrameworkService(final DashboardFrameworkEntityService dashboardFrameworkEntityService) { + super(); + this.dashboardFrameworkEntityService = dashboardFrameworkEntityService; + } + + /** + * Create a new dashboard framework + * @param framework The dashboard framework to create + * @return The created dashboard framework + */ + @Override + public DashboardFramework create(DashboardFramework framework) { + DashboardFrameworkEntity entity = dashboardFrameworkEntityService.create(DashboardFrameworkEntity.fromPojo(framework)); + return entity.toPojo(); + } + + /** + * Update an existing dashboard framework + * @param framework The dashboard framework to update + * @return The updated dashboard framework + * @throws NotFoundException When the dashboard framework cannot be found + */ + @Override + public DashboardFramework update(DashboardFramework framework) throws NotFoundException { + Optional existing = Optional.ofNullable(dashboardFrameworkEntityService.retrieve(framework.getId())); + + if (existing.isPresent()) { + existing.get().update(framework); + dashboardFrameworkEntityService.update(existing.get()); + } else { + throw new NotFoundException("DashboardFramework", framework.getId()); + } + + return dashboardFrameworkEntityService.retrieve(framework.getId()).toPojo(); + } + + /** + * Get a dashboard framework by ID + * @param id The ID of the dashboard framework to get + * @return The dashboard framework if found, otherwise null + */ + @Override + public Optional get(Long id) { + return Optional.ofNullable(dashboardFrameworkEntityService.retrieve(id)) + .map(DashboardFrameworkEntity::toPojo); + } + + /** + * Get a dashboard framework by name + * @param name The name of the dashboard framework to get + * @return The dashboard framework if found, otherwise null + */ + @Override + public Optional get(String name) { + return dashboardFrameworkEntityService.findFrameworkByName(name) + .map(DashboardFrameworkEntity::toPojo); + } + + /** + * Get all dashboard frameworks + * @return A list of all dashboard frameworks + */ + @Override + public List getAll() { + return dashboardFrameworkEntityService.getAll().stream() + .map(DashboardFrameworkEntity::toPojo) + .collect(java.util.stream.Collectors.toList()); + } + + /** + * Delete a dashboard framework by ID + * @param id The ID of the dashboard framework to delete + */ + @Override + public void delete(Long id) { + dashboardFrameworkEntityService.delete(id); + } + + /** + * Delete a dashboard framework by name + * @param name The name of the dashboard framework to delete + */ + @Override + public void delete(String name) { + get(name).map(DashboardFramework::getId) + .ifPresent(this::delete); + } + + /** + * Resolve the command for a dashboard. If the dashboard framework is "custom", the command is taken from the + * dashboard. Otherwise, the command is taken from the dashboard framework. The variables {repo}, {repobranch}, + * and {mainFilePath} are replaced with the dashboard's git repo URL, git repo branch, and main file path, + * respectively. + * + * @param dashboard The dashboard to resolve the command for + * @return The resolved command + */ + @Override + public String resolveCommand(Dashboard dashboard) { + String command; + + if (StringUtils.isBlank(dashboard.getFramework()) || dashboard.getFramework().equalsIgnoreCase("custom")) { + command = dashboard.getCommand(); + } else { + final DashboardFramework framework = get(dashboard.getFramework()).orElseThrow(() -> new IllegalArgumentException("DashboardFramework " + dashboard.getFramework() + " does not exist")); + command = framework.getCommandTemplate(); + } + + command = command.replaceAll("\\{repo\\}", dashboard.getGitRepoUrl()) + .replaceAll("\\{repobranch\\}", dashboard.getGitRepoBranch()) + .replaceAll("\\{mainFilePath\\}", dashboard.getMainFilePath()); + + // remove extra spaces + command = removeExtraSpaces(command); + + return command; + } + + /** + * Replace multiple spaces with a single space + * @param command The command to remove extra spaces from + * @return The command with extra spaces removed + */ + protected String removeExtraSpaces(String command) { + return command.replaceAll("\\s+", " "); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardJobTemplateService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardJobTemplateService.java new file mode 100644 index 0000000..fc7752c --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardJobTemplateService.java @@ -0,0 +1,164 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.nrg.framework.constants.Scope; +import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; +import org.nrg.xnat.compute.models.HardwareConfig; +import org.nrg.xnat.compute.models.JobTemplate; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; +import org.nrg.xnat.compute.services.ConstraintConfigService; +import org.nrg.xnat.compute.services.HardwareConfigService; +import org.nrg.xnat.compute.services.impl.DefaultJobTemplateService; +import org.nrg.xnatx.plugins.jupyterhub.models.Dashboard; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardConfig; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardJobTemplateService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * This works in conjunction with the JobTemplateService to resolve a job template for a dashboard config. Dashboards + * availability is handled separately from compute environment and hardware availability checks. Any compute environment + * and hardware can be used for a dashboard as long as the dashboard config is available to the given execution scope. + */ +@Service +@Slf4j +public class DefaultDashboardJobTemplateService extends DefaultJobTemplateService implements DashboardJobTemplateService { + + private final DashboardConfigService dashboardConfigService; + private final ComputeEnvironmentConfigService computeEnvironmentConfigService; + private final DashboardFrameworkService dashboardFrameworkService; + + @Autowired + public DefaultDashboardJobTemplateService(final ComputeEnvironmentConfigService computeEnvironmentConfigService, + final HardwareConfigService hardwareConfigService, + final ConstraintConfigService constraintConfigService, + final DashboardConfigService dashboardConfigService, + final DashboardFrameworkService dashboardFrameworkService) { + super(computeEnvironmentConfigService, hardwareConfigService, constraintConfigService); + this.dashboardConfigService = dashboardConfigService; + this.computeEnvironmentConfigService = computeEnvironmentConfigService; + this.dashboardFrameworkService = dashboardFrameworkService; + } + + /** + * Override the availability check for a compute environment and hardware config. They are always available for a + * dashboard config. + * @param computeEnvironmentConfigId the compute environment config id + * @param hardwareConfigId the hardware config id + * @param executionScope the execution scope to verify the compute environment config and hardware are + * @return True if the compute environment and hardware are available, false otherwise + */ + @Override + public boolean isAvailable(Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope) { + // For dashboard, availability is stored in the dashboard config. Override the compute environment and hardware + // availability checks. Any compute environment and hardware can be used for a dashboard as long as the + // dashboard config is available to the given execution scope. + return true; + } + + /** + * Resolve a job template for a dashboard config. The command is taken from the dashboard config. The variables + * @param computeEnvironmentConfigId the compute environment config id + * @param hardwareConfigId the hardware config id + * @param executionScope the execution scope to verify the compute environment config and hardware are + * available + * @return + */ + @Override + public JobTemplate resolve(Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope) { + // Should resolve the job template and override the compute environment and hardware availability checks. + return super.resolve(computeEnvironmentConfigId, hardwareConfigId, executionScope); + } + + /** + * Check if a dashboard config is available given the execution scope (site, project, datatype, ...). The dashboard config + * @param dashboardConfigId The ID of the dashboard config to check availability for + * @param computeEnvironmentConfigId The ID of the compute environment config to check availability for + * @param hardwareConfigId The ID of the hardware config to check availability for + * @param executionScope The execution scope to check availability for + * @return True if the dashboard config is available, false otherwise + */ + @Override + public boolean isAvailable(Long dashboardConfigId, Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope) { + // Check if the dashboard config is available given the execution scope (site, project, ...) + boolean isDashboardAvailable = dashboardConfigService.isAvailable(dashboardConfigId, executionScope); + + if (!isDashboardAvailable) { + return false; + } + + // Check if the compute environment config and hardware config are available to the dashboard config + DashboardConfig dashboardConfig = dashboardConfigService.retrieve(dashboardConfigId).orElse(null); + + if (dashboardConfig == null || + dashboardConfig.getComputeEnvironmentConfig() == null || + dashboardConfig.getComputeEnvironmentConfig().getId() == null || + dashboardConfig.getHardwareConfig() == null || + dashboardConfig.getHardwareConfig().getId() == null) { + throw new IllegalArgumentException("DashboardConfig, ComputeEnvironmentConfig, and/or HardwareConfig ids must be specified"); + } + + boolean isComputeEnvironmentConfigAvailable = dashboardConfig.getComputeEnvironmentConfig().getId().equals(computeEnvironmentConfigId); + boolean isHardwareConfigAvailable = dashboardConfig.getHardwareConfig().getId().equals(hardwareConfigId); + + if (!isComputeEnvironmentConfigAvailable || !isHardwareConfigAvailable) { + return false; + } + + // Check if the hardware config is available to the compute environment config + ComputeEnvironmentConfig computeEnvironmentConfig = computeEnvironmentConfigService + .retrieve(computeEnvironmentConfigId) + .orElseThrow(() -> new IllegalArgumentException("ComputeEnvironmentConfig with id " + computeEnvironmentConfigId + " does not exist")); + + if (computeEnvironmentConfig.getHardwareOptions().isAllowAllHardware()) { + return true; + } + + return computeEnvironmentConfig.getHardwareOptions() + .getHardwareConfigs().stream() + .map(HardwareConfig::getId) + .anyMatch(id -> id.equals(hardwareConfigId)); + } + + /** + * Resolve a job template for a dashboard config. The command is taken from the dashboard config. The variables + * @param dashboardConfigId The ID of the dashboard config to resolve the job template for + * @param computeEnvironmentConfigId The ID of the compute environment config to resolve the job template for + * @param hardwareConfigId The ID of the hardware config to resolve the job template for + * @param executionScope The execution scope to resolve the job template for + * @return The resolved job template + */ + @Override + public JobTemplate resolve(Long dashboardConfigId, Long computeEnvironmentConfigId, Long hardwareConfigId, Map executionScope) { + if (dashboardConfigId == null || computeEnvironmentConfigId == null || hardwareConfigId == null) { + throw new IllegalArgumentException("DashboardConfig, ComputeEnvironmentConfig, and HardwareConfig ids must be specified"); + } + + if (!isAvailable(dashboardConfigId, computeEnvironmentConfigId, hardwareConfigId, executionScope)) { + throw new IllegalArgumentException("DashboardConfig, ComputeEnvironmentConfig, and HardwareConfig are not available"); + } + + JobTemplate jobTemplate = resolve(computeEnvironmentConfigId, hardwareConfigId, executionScope); + + // Override the command in the job template with the command from the dashboard config + DashboardConfig dashboardConfig = dashboardConfigService.retrieve(dashboardConfigId).orElse(null); + + if (dashboardConfig != null && + dashboardConfig.getDashboard() != null) { + + Dashboard dashboard = dashboardConfig.getDashboard(); + String command = dashboardFrameworkService.resolveCommand(dashboard); + + if (!StringUtils.isBlank(command)) { + jobTemplate.getComputeEnvironment().setCommand(command); + } + } + + return jobTemplate; + } +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java index 58c7f71..3cc2bd1 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubService.java @@ -1,15 +1,14 @@ package org.nrg.xnatx.plugins.jupyterhub.services.impl; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.nrg.framework.constants.Scope; import org.nrg.framework.services.NrgEventServiceI; import org.nrg.xdat.XDAT; -import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xdat.security.user.exceptions.UserInitException; import org.nrg.xft.security.UserI; +import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xnat.utils.FileUtils; import org.nrg.xnatx.plugins.jupyterhub.client.JupyterHubClient; import org.nrg.xnatx.plugins.jupyterhub.client.exceptions.ResourceAlreadyExistsException; @@ -23,14 +22,15 @@ import org.nrg.xnatx.plugins.jupyterhub.models.ServerStartRequest; import org.nrg.xnatx.plugins.jupyterhub.models.XnatUserOptions; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardJobTemplateService; import org.nrg.xnatx.plugins.jupyterhub.services.JupyterHubService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.utils.JupyterHubServiceAccountHelper; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -41,7 +41,6 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; -import java.util.stream.Stream; @SuppressWarnings("BusyWait") @Service @@ -55,6 +54,7 @@ public class DefaultJupyterHubService implements JupyterHubService { private final JupyterHubPreferences jupyterHubPreferences; private final UserManagementServiceI userManagementService; private final JobTemplateService jobTemplateService; + private final DashboardJobTemplateService dashboardJobTemplateService; private final JupyterHubServiceAccountHelper jupyterHubServiceAccountHelper; @Autowired @@ -64,7 +64,8 @@ public DefaultJupyterHubService(final JupyterHubClient jupyterHubClient, final UserOptionsService userOptionsService, final JupyterHubPreferences jupyterHubPreferences, final UserManagementServiceI userManagementService, - final JobTemplateService jobTemplateService, + @Qualifier("defaultJobTemplateService") final JobTemplateService jobTemplateService, + final DashboardJobTemplateService dashboardJobTemplateService, final JupyterHubServiceAccountHelper jupyterHubServiceAccountHelper) { this.jupyterHubClient = jupyterHubClient; this.eventService = eventService; @@ -73,6 +74,7 @@ public DefaultJupyterHubService(final JupyterHubClient jupyterHubClient, this.jupyterHubPreferences = jupyterHubPreferences; this.userManagementService = userManagementService; this.jobTemplateService = jobTemplateService; + this.dashboardJobTemplateService = dashboardJobTemplateService; this.jupyterHubServiceAccountHelper = jupyterHubServiceAccountHelper; } @@ -169,22 +171,35 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) final String eventTrackingId = startRequest.getEventTrackingId(); final Long computeEnvironmentConfigId = startRequest.getComputeEnvironmentConfigId(); final Long hardwareConfigId = startRequest.getHardwareConfigId(); + final Long dashboardConfigId = startRequest.getDashboardConfigId(); + + final String application = dashboardConfigId != null ? "dashboard" : "Jupyter notebook"; eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, 0, - "Starting Jupyter notebook server for user " + user.getUsername())); + "Starting " + application + " for user " + user.getUsername() + ".")); if (!permissionsHelper.canRead(user, projectId, itemId, xsiType)) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Failed to launch Jupyter notebook server. Permission denied to read " + xsiType + " " + itemId + " in project " + projectId)); + "Failed to launch " + application + ". Permission denied to read " + xsiType + " " + itemId + " in project " + projectId)); return; } Map executionScope = new HashMap<>(); executionScope.put(Scope.Project, projectId); executionScope.put(Scope.User, user.getUsername()); - if (!jobTemplateService.isAvailable(computeEnvironmentConfigId, hardwareConfigId, executionScope)) { + executionScope.put(Scope.DataType, xsiType); + executionScope.put(Scope.Site, "XNAT"); + + if (dashboardConfigId != null) { + if (!dashboardJobTemplateService.isAvailable(dashboardConfigId, computeEnvironmentConfigId, hardwareConfigId, executionScope)) { + eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, + JupyterServerEventI.Operation.Start, + "Failed to start dashboard. The dashboard is not available to the user.")); + return; + } + } else if (!jobTemplateService.isAvailable(computeEnvironmentConfigId, hardwareConfigId, executionScope)) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, "Failed to launch Jupyter notebook server. The compute environment or hardware configuration is not available to the user.")); @@ -224,7 +239,7 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) // We don't want to update the user options entity if there is a running server eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, 0, - "Checking for existing Jupyter notebook servers.")); + "Checking for existing Jupyter servers.")); boolean hasRunningServer = jupyterHubClient.getUser(user.getUsername()) .orElseGet(() -> createUser(user)) @@ -234,17 +249,17 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Failed to launch Jupyter notebook server. " + - "There is already one running. " + - "Please stop the running server before starting a new one.")); + "Failed to launch " + application + ". " + + "There is already a dashboard or Jupyter notebook running. " + + "Please stop the running dashboard or notebook before starting a new one.")); return; } eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, 20, - "Building notebook server container configuration.")); + "Building container configuration.")); - userOptionsService.storeUserOptions(user, servername, xsiType, itemId, projectId, computeEnvironmentConfigId, hardwareConfigId, eventTrackingId); + userOptionsService.storeUserOptions(user, servername, xsiType, itemId, projectId, computeEnvironmentConfigId, hardwareConfigId, dashboardConfigId, eventTrackingId); eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, 30, @@ -260,11 +275,12 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) .itemLabel(itemLabel) .projectId(projectId) .eventTrackingId(eventTrackingId) + .dashboardConfigId(dashboardConfigId) .build()); eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, 40, - "JupyterHub is spawning notebook server container.")); + "JupyterHub is spawning container.")); // TODO consume progress api int time = 0; @@ -275,10 +291,11 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) if (server.isPresent()) { if (server.get().getReady()) { - log.info("Jupyter server started for user {}", user.getUsername()); + log.info("{} started for user: {}, xsiType: {}, itemId: {}, projectId: {}, computeEnvironmentConfigId: {}, hardwareConfigId: {}, dashboardConfigId: {}.", + StringUtils.capitalize(application), user.getUsername(), xsiType, itemId, projectId, computeEnvironmentConfigId, hardwareConfigId, dashboardConfigId); eventService.triggerEvent(JupyterServerEvent.completed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Jupyter notebook server is available at: " + server.get().getUrl())); + StringUtils.capitalize(application) + " is available at: " + server.get().getUrl())); return; } } else { @@ -292,24 +309,24 @@ public void startServer(final UserI user, final ServerStartRequest startRequest) eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Failed to launch Jupyter notebook server. " + + "Failed to launch " + application + "." + "Timeout exceeded while waiting for JupyterHub to spawn server. " + "Check the XNAT and JupyterHub system logs for error messages.")); } catch (UserNotFoundException e) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Failed to launch Jupyter notebook server. User not found.")); + "Failed to launch " + application + ". User not found.")); } catch (ResourceAlreadyExistsException e) { eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, - "Failed to launch Jupyter notebook server. A server with the same name is already running.")); + "Failed to launch " + application + ". A server with the same name is already running.")); } catch (InterruptedException e) { - String msg = "Failed to launch Jupyter notebook server. Thread interrupted. Check the XNAT and JupyterHub system logs for error messages."; + String msg = "Failed to launch " + application + ". Thread interrupted. Check the XNAT and JupyterHub system logs for error messages."; eventService.triggerEvent(JupyterServerEvent.failed(eventTrackingId, user.getID(), xsiType, itemId, JupyterServerEventI.Operation.Start, msg)); log.error(msg, e); } catch (Exception e) { - String msg = "Failed to launch Jupyter notebook server. "; + String msg = "Failed to launch " + application + ". "; if (!jupyterHubServiceAccountHelper.isJupyterHubServiceAccountEnabled()) { msg += "Make sure the JupyterHub service account user is enabled and provide the credentials to JupyterHub (refer to the documentation for more details). "; @@ -407,7 +424,7 @@ public void stopServer(final UserI user, String eventTrackingId) { public void stopServer(final UserI user, final String servername, String eventTrackingId) { eventService.triggerEvent(JupyterServerEvent.progress(eventTrackingId, user.getID(), JupyterServerEventI.Operation.Stop, 0, - "Stopping Jupyter Notebook Server.")); + "Stopping Jupyter Server.")); CompletableFuture.runAsync(() -> { try { diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java index 3b95a76..36c0157 100644 --- a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultUserOptionsService.java @@ -27,11 +27,13 @@ import org.nrg.xnatx.plugins.jupyterhub.models.docker.Mount; import org.nrg.xnatx.plugins.jupyterhub.models.docker.*; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardJobTemplateService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsEntityService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.services.UserWorkspaceService; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import javax.annotation.Nullable; @@ -55,6 +57,7 @@ public class DefaultUserOptionsService implements UserOptionsService { private final UserOptionsEntityService userOptionsEntityService; private final PermissionsHelper permissionsHelper; private final JobTemplateService jobTemplateService; + private final DashboardJobTemplateService dashboardJobTemplateService; @Autowired public DefaultUserOptionsService(final JupyterHubPreferences jupyterHubPreferences, @@ -64,7 +67,8 @@ public DefaultUserOptionsService(final JupyterHubPreferences jupyterHubPreferenc final SiteConfigPreferences siteConfigPreferences, final UserOptionsEntityService userOptionsEntityService, final PermissionsHelper permissionsHelper, - final JobTemplateService jobTemplateService) { + @Qualifier("defaultJobTemplateService") final JobTemplateService jobTemplateService, + final DashboardJobTemplateService dashboardJobTemplateService) { this.jupyterHubPreferences = jupyterHubPreferences; this.userWorkspaceService = userWorkspaceService; this.searchHelperService = searchHelperService; @@ -73,6 +77,7 @@ public DefaultUserOptionsService(final JupyterHubPreferences jupyterHubPreferenc this.userOptionsEntityService = userOptionsEntityService; this.permissionsHelper = permissionsHelper; this.jobTemplateService = jobTemplateService; + this.dashboardJobTemplateService = dashboardJobTemplateService; } public Map getProjectPaths(final UserI user, final List projectIds) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException { @@ -302,7 +307,7 @@ public Optional retrieveUserOptions(UserI user, String serverna @Override public void storeUserOptions(UserI user, String servername, String xsiType, String id, String projectId, - Long computeEnvironmentConfigId, Long hardwareConfigId, String eventTrackingId) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException { + Long computeEnvironmentConfigId, Long hardwareConfigId, Long dashboardConfigId, String eventTrackingId) throws BaseXnatExperimentdata.UnknownPrimaryProjectException, DBPoolException, SQLException, InvalidArchiveStructure, IOException { log.debug("Storing user options for user '{}' server '{}' xsiType '{}' id '{}' projectId '{}'", user.getUsername(), servername, xsiType, id, projectId); @@ -314,7 +319,17 @@ public void storeUserOptions(UserI user, String servername, String xsiType, Stri Map executionScope = new HashMap<>(); executionScope.put(Scope.Project, projectId); executionScope.put(Scope.User, user.getUsername()); - JobTemplate jobTemplate = jobTemplateService.resolve(computeEnvironmentConfigId, hardwareConfigId, executionScope); + executionScope.put(Scope.DataType, xsiType); + + JobTemplate jobTemplate; + + if (dashboardConfigId != null) { + // The dashboard resolver supersedes the compute environment command, + // enabling the initiation of the dashboard rather than Jupyter Lab + jobTemplate = dashboardJobTemplateService.resolve(dashboardConfigId, computeEnvironmentConfigId, hardwareConfigId, executionScope); + } else { + jobTemplate = jobTemplateService.resolve(computeEnvironmentConfigId, hardwareConfigId, executionScope); + } // specific xsi type -> general xsi type if (instanceOf(xsiType, XnatExperimentdata.SCHEMA_ELEMENT_NAME)) { @@ -489,6 +504,7 @@ protected TaskTemplate toTaskTemplate(@NonNull JobTemplate jobTemplate) { // Build container spec for the task template containerSpec.setImage(computeEnvironment.getImage()); + containerSpec.setCommand(computeEnvironment.getCommand()); // Add environment variables from compute environment and hardware Map environmentVariables = new HashMap<>(); diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardConfigEntityService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardConfigEntityService.java new file mode 100644 index 0000000..27694fb --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardConfigEntityService.java @@ -0,0 +1,46 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; +import org.nrg.xnatx.plugins.jupyterhub.entities.DashboardConfigEntity; +import org.nrg.xnatx.plugins.jupyterhub.repositories.DashboardConfigDao; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigEntityService; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Slf4j +public class HibernateDashboardConfigEntityService extends AbstractHibernateEntityService implements DashboardConfigEntityService { + + // For testing + public HibernateDashboardConfigEntityService(final DashboardConfigDao dao) { + super(); + setDao(dao); + } + + /** + * Checks if a compute environment config is in use. + * + * @param id The ID of the compute environment config to check. + * @return True if the compute environment config is used by a dashboard config, otherwise false. + */ + @Override + @Transactional + public boolean isComputeEnvironmentConfigInUse(Long id) { + return getDao().isComputeEnvironmentConfigInUse(id); + } + + /** + * Checks if a hardware config is in use. + * + * @param id The ID of the hardware config to check. + * @return True if the hardware config is used by a dashboard config, otherwise false. + */ + @Override + @Transactional + public boolean isHardwareConfigInUse(Long id) { + return getDao().isHardwareConfigInUse(id); + } + +} diff --git a/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardFrameworkEntityService.java b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardFrameworkEntityService.java new file mode 100644 index 0000000..c2c78ab --- /dev/null +++ b/src/main/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardFrameworkEntityService.java @@ -0,0 +1,34 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import lombok.extern.slf4j.Slf4j; +import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; +import org.nrg.xnatx.plugins.jupyterhub.entities.DashboardFrameworkEntity; +import org.nrg.xnatx.plugins.jupyterhub.repositories.DashboardFrameworkEntityDao; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkEntityService; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.Optional; + +@Service +@Transactional +@Slf4j +public class HibernateDashboardFrameworkEntityService extends AbstractHibernateEntityService implements DashboardFrameworkEntityService { + + // For testing + public HibernateDashboardFrameworkEntityService(final DashboardFrameworkEntityDao dao) { + super(); + setDao(dao); + } + + /** + * Finds a dashboard framework by name. + * @param name The name of the dashboard framework to find. + * @return The dashboard framework if found, otherwise empty optional. + */ + @Override + public Optional findFrameworkByName(String name) { + return getDao().findFrameworkByName(name); + } + +} diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-dashboards.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-dashboards.js new file mode 100644 index 0000000..1a9c6a8 --- /dev/null +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-dashboards.js @@ -0,0 +1,1292 @@ +/** + * JupyterHub Dashboard Functions + */ +console.debug('Loading jupyterhub-dashboards.js'); + +var XNAT = getObject(XNAT || {}); +XNAT.app = getObject(XNAT.app || {}); +XNAT.app.activityTab = getObject(XNAT.app.activityTab || {}); +XNAT.plugin = getObject(XNAT.plugin || {}); +XNAT.plugin.jupyterhub = getObject(XNAT.plugin.jupyterhub || {}); +XNAT.plugin.jupyterhub.dashboards = getObject(XNAT.plugin.jupyterhub.dashboards || {}); +XNAT.plugin.jupyterhub.dashboards.configs = getObject(XNAT.plugin.jupyterhub.dashboards.configs || {}); +XNAT.plugin.jupyterhub.dashboards.dataTypes = getObject(XNAT.plugin.jupyterhub.dashboards.dataTypes || {}); +XNAT.plugin.jupyterhub.dashboards.frameworks = getObject(XNAT.plugin.jupyterhub.dashboards.frameworks || {}); + +(function(factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } + else if (typeof exports === 'object') { + module.exports = factory(); + } + else { + return factory(); + } +}(function() { + + XNAT.plugin.jupyterhub.dashboards.dataTypes = { + getAll: async function () { + return new Promise((resolve, reject) => XNAT.app.dataTypeAccess.getElements.createable.ready(resolve, reject)); + }, + getSome: async function () { + // Get only the data types that can be used to create dashboards + return this.getAll().then((dataTypes) => { + return dataTypes.elements.filter((dataType) => { + return dataType['experiment'] === true || + dataType['elementName'] === "xnat:subjectData" || + dataType['elementName'] === "xnat:projectData" + }); + }); + } + } + + XNAT.plugin.jupyterhub.dashboards.configs = { + url: `/xapi/jupyterhub/dashboards/configs`, + get: async function (id) { + const url = XNAT.url.csrfUrl(`${this.url}/${id}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error(`Failed to get dashboard config with id ${id}`); + } + }, + getAll: async function () { + const url = XNAT.url.csrfUrl(`${this.url}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + const dashboardConfigs = await response.json(); + dashboardConfigs.sort((a, b) => a.dashboard.name.localeCompare(b.dashboard.name)); + return dashboardConfigs; + } else { + throw new Error(`Failed to get dashboard configs`); + } + }, + create: async function (dashboardConfig) { + const url = XNAT.url.csrfUrl(`${this.url}`); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(dashboardConfig) + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error(`Failed to create dashboard config`); + } + }, + update: async function (dashboardConfig) { + const url = XNAT.url.csrfUrl(`${this.url}/${dashboardConfig.id}`); + + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(dashboardConfig) + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error(`Failed to update dashboard config`); + } + }, + delete: async function (id) { + const url = XNAT.url.csrfUrl(`${this.url}/${id}`); + + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Failed to delete dashboard config with id ${id}`); + } + }, + available: async function (executionScope) { + console.debug(`Getting available dashboard configs for execution scope ${executionScope}`); + + let url = XNAT.url.csrfUrl(`${this.url}/available?`); + + // add each scope to the url as a query parameter + for (const scope in executionScope) { + if (executionScope.hasOwnProperty(scope)) { + const value = executionScope[scope]; + if (value) { + url += `&${scope}=${value}`; + } + } + } + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Failed to get available dashboard configs for execution scope ${executionScope}`); + } + + // Sort by name + const dashboardConfigs = await response.json(); + dashboardConfigs.sort((a, b) => a.dashboard.name.localeCompare(b.dashboard.name)); + return dashboardConfigs; + }, + enableForSite: async function (id) { + const url = XNAT.url.csrfUrl(`${this.url}/${id}/scope/site`); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Failed to enable dashboard config with id ${id} for site`); + } + }, + disableForSite: async function (id) { + const url = XNAT.url.csrfUrl(`${this.url}/${id}/scope/site`); + + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Failed to disable dashboard config with id ${id} for site`); + } + }, + enableForProject: async function (id, projectId) { + const url = XNAT.url.csrfUrl(`${this.url}/${id}/scope/project/${projectId}`); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Failed to enable dashboard config with id ${id} for project ${projectId}`); + } + }, + disableForProject: async function (id, projectId) { + const url = XNAT.url.csrfUrl(`${this.url}/${id}/scope/project/${projectId}`); + + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Failed to disable dashboard config with id ${id} for project ${projectId}`); + } + }, + editor: async function(dashboardConfig, action, onSaved) { + let isNew = action === 'create', + isCopy = action === 'copy', + isEdit = action === 'edit', + title = isNew || isCopy ? 'New Dashboard' : 'Edit Dashboard'; + + XNAT.dialog.open({ + title: title, + content: spawn('div.dashboard-editor'), + width: 750, + maxBtn: true, + beforeShow: () => { + // Create form + + const formContainer = document.querySelector(`.dashboard-editor`); + formContainer.classList.add('panel'); + + dashboardConfig = dashboardConfig ? dashboardConfig : {}; + let dashboard = dashboardConfig?.dashboard ? dashboardConfig?.dashboard : {}; + + let id = isNew || isCopy ? '' : dashboardConfig?.id ?? '', + name = isCopy ? '' : dashboard?.name ?? '', + description = dashboard?.description ?? '', + framework = dashboard?.framework ?? '', + command = dashboard?.command ?? '', + gitRepoUrl = dashboard?.gitRepoUrl ?? '', + gitRepoBranch = dashboard?.gitRepoBranch ?? '', + mainFilePath = dashboard?.mainFilePath ?? '', + computeEnvironmentId = dashboardConfig?.computeEnvironmentConfig?.id ?? '', + hardwareId = dashboardConfig?.hardwareConfig?.id ?? ''; + + let form = spawn('form.dashboard-edit-form', [ + spawn('style|type=text/css', ` + .panel .panel-element input[type="text"], + .panel .panel-element select, + .panel .panel-element textarea, + .panel .panel-element .description { + width: 400px; + } + + .panel .panel-element select[multiple] { + max-width: 400px; + height: 150px; + } + + hr { + margin: 15px 25px; + } + + code { + white-space: pre-wrap; + } + `), + spawn('input#id', { type: 'hidden', value: id }), + spawn('div.panel-element|data-name=name', [ + spawn('label.element-label|for=name', 'Name'), + spawn('div.element-wrapper', [ + spawn(`input#name|type=text`, { value: name }), + spawn('div.description', 'Provide a name for the dashboard. This will be displayed to users.') + ]), + spawn('div.clear') + ]), + spawn('div.panel-element|data-name=description', [ + spawn('label.element-label|for=description', 'Description'), + spawn('div.element-wrapper', [ + spawn( + `textarea#description|rows=3`, + { style: { fontFamily: 'sans-serif' } }, + description + ), + spawn('div.description', '(Optional) Provide a description for the dashboard. This will be displayed to users.') + ]), + spawn('div.clear') + ]), + spawn('hr'), + spawn('div.panel-element.file-source.git|data-name=git-repo-url', [ + spawn('label.element-label|for=git-repo-url', 'Git Repo URL'), + spawn('div.element-wrapper', [ + spawn(`input#git-repo-url|type=text`, { value: gitRepoUrl }), + spawn('div.description', 'Enter the URL of the Git repo that contains the dashboard code.') + ]), + spawn('div.clear') + ]), + spawn('div.panel-element.file-source.git|data-name=git-repo-branch', [ + spawn('label.element-label|for=git-repo-branch', 'Branch'), + spawn('div.element-wrapper', [ + spawn(`input#git-repo-branch|type=text`, { value: gitRepoBranch }), + spawn('div.description', 'Enter the branch containing the dashboard code.') + ]), + spawn('div.clear') + ]), + spawn('div.panel-element.file-source.git|data-name=main-file-path', [ + spawn('label.element-label|for=main-file-path', 'Main File Path'), + spawn('div.element-wrapper', [ + spawn(`input#main-file-path|type=text`, { value: mainFilePath }), + spawn('div.description', 'Enter the path to the main file in the Git repository.') + ]), + spawn('div.clear') + ]), + spawn('hr'), + spawn('div.panel-element|data-name=framework', [ + spawn('label.element-label|for=framework', 'Framework'), + spawn('div.element-wrapper', [ + spawn('select#framework', + { + onchange: (e) => { + const advanced = document.querySelectorAll('.panel-element.advanced'); + advanced.forEach((element) => { + if (e.target.value.toLowerCase() === 'custom') { + element.style.display = 'block'; + } else { + element.style.display = 'none'; + } + }) + } + }, [ + spawn('option', { value: '', disabled: true, selected: true }, 'Select a dashboard framework'), + ]), + spawn('div.description', 'Select which dashboard framework is used to run the dashboard.') + ]), + spawn('div.clear') + ]), + spawn('div.panel-element.advanced|data-name=command', {style: { display: 'none' } }, [ + spawn('label.element-label|for=command', 'Command'), + spawn('div.element-wrapper', [ + spawn( + `textarea#command|rows=6`, + { style: { fontFamily: 'sans-serif' } }, + command + ), + spawn('div.description', + 'Enter the command that will be executed to start the dashboard. ' + + 'Use the following placeholders to insert values into the command: ' + + '

    • {repo} - The URL of the Git repository
    • {repobranch} - ' + + 'The branch of the Git repository
    • {mainFilePath} - The path to the' + + ' main file in the Git repository
    ' + + 'Use the jh-single-native-proxy python package to proxy the dashboard ' + + 'container through JupyterHub.' + + '

    ' + + 'Example:
    ' + + '' + + 'jhsingle-native-proxy\n' + + '\t--destport 8505\n' + + '\t--repo {repo}\n' + + '\t--repobranch {repobranch}\n' + + '\t--repofolder /home/jovyan/dashboards\n' + + 'streamlit run\n' + + '\t/home/jovyan/dashboards/{mainFilePath}\n' + + '\t{--}server.port 8505\n' + + '\t{--}server.headless True\n' + + '' + + '

    ' + + 'See documentation for more details.' + ) + ]), + spawn('div.clear') + ]), + spawn('hr'), + spawn('div.panel-element|data-name=jupyter-environment', [ + spawn('label.element-label|for=jupyter-environment', 'Jupyter Environment'), + spawn('div.element-wrapper', [ + spawn('select#jupyter-environment', [ + spawn('option', { value: '', disabled: true, selected: true }, 'Select a Jupyter environment') + ]), + spawn('div.description', 'The dashboard will be started in this Jupyter environment.') + ]), + spawn('div.clear') + ]), + spawn('div.panel-element|data-name=hardware', [ + spawn('label.element-label|for=hardware', 'Hardware'), + spawn('div.element-wrapper', [ + spawn('select#hardware', [ + spawn('option', { value: '', disabled: true, selected: true }, 'Select hardware') + ]), + spawn('div.description', 'The dashboard will be started with this hardware configuration.') + ]), + spawn('div.clear') + ]), + spawn('hr'), + spawn('div.panel-element|data-name=data-types', [ + spawn('label.element-label|for=data-types', 'Data Types'), + spawn('div.element-wrapper', [ + spawn('select#data-types', { multiple: true }, []), + spawn('div.description', 'Select the XNAT datatypes that this dashboard can be started from.') + ]), + spawn('div.clear') + ]), + ]); + + formContainer.appendChild(form); + + // Handle Jupyter Environments and Hardware + let computeEnvironments = document.querySelector('#jupyter-environment'); + let hardwares = document.querySelector('#hardware'); + + XNAT.compute.computeEnvironmentConfigs.getAll("JUPYTERHUB").then((computeEnvironmentConfigs) => { + computeEnvironmentConfigs.forEach((config) => { + let option = document.createElement('option'); + option.value = config.id; + option.text = config.computeEnvironment.name; + option.selected = config.id === computeEnvironmentId; + computeEnvironments.add(option); + }) + + return computeEnvironmentConfigs; + }).then((computeEnvironmentConfigs) => { + computeEnvironments.addEventListener('change', () => { + hardwares.innerHTML = ''; + hardwares.appendChild(spawn('option', {value: ''}, 'Select hardware')); + let computeEnvironmentConfig = computeEnvironmentConfigs.filter(c => c['id'].toString() === computeEnvironments.value)[0]; + console.debug(computeEnvironmentConfig); + let hardwareConfigs = computeEnvironmentConfig['hardwareOptions']['hardwareConfigs']; + console.debug(hardwareConfigs); + hardwareConfigs.forEach(h => { + let option = document.createElement('option'); + option.value = h['id']; + option.text = h['hardware']['name']; + option.selected = h['id'] === hardwareId; + console.debug(option) + hardwares.add(option); + }); + }) + + // Trigger change event to populate hardware options if compute environment + // is already selected (i.e. editing) + if (computeEnvironmentId) { + computeEnvironments.dispatchEvent(new Event('change')); + } + + // Add dashboard frameworks + let frameworksSelector = document.querySelector('#framework'); + XNAT.plugin.jupyterhub.dashboards.frameworks.getAll().then((frameworks) => { + frameworks.forEach((framework) => { + let option = document.createElement('option'); + option.value = framework['name']; + option.text = framework['name']; + option.selected = dashboardConfig?.dashboard?.framework === framework['name']; + frameworksSelector.add(option); + }) + + // Add Custom option + let option = document.createElement('option'); + option.value = 'Custom'; + option.text = 'Custom'; + option.selected = + dashboardConfig?.dashboard?.framework === 'Custom' || + dashboardConfig?.dashboard?.framework === 'custom' || + ((isEdit || isCopy) && !dashboardConfig?.dashboard?.framework); + frameworksSelector.add(option); + + // Trigger change event to populate custom command if framework is already selected (i.e. editing) + document.querySelector('#framework').dispatchEvent(new Event('change')); + }); + + // Add data types + let dataTypesSelector = document.querySelector('#data-types'); + XNAT.plugin.jupyterhub.dashboards.dataTypes.getSome().then((dataTypes) => { + dataTypes.forEach((dataType) => { + let option = document.createElement('option'); + option.value = dataType['elementName']; + option.text = dataType['plural']; + option.selected = dashboardConfig?.scopes?.DataType?.ids?.includes(dataType['elementName']) ?? false; + dataTypesSelector.add(option); + }) + }); + }); + }, + buttons: [ + { + label: 'Cancel', + isDefault: false, + close: false, + action: function () { + XNAT.dialog.closeAll(); + } + }, + { + label: isNew || isCopy ? 'Create Dashboard' : 'Save Dashboard', + isDefault: true, + close: false, + action: function (obj) { + let form = document.querySelector('.dashboard-edit-form'); + + const validators = []; + + validators.push( + XNAT.validate(form.querySelector('#name')) + .reset().chain() + .required() + .is('notEmpty') + .failure('Name is required') + ); + + validators.push( + XNAT.validate(form.querySelector('#framework')) + .reset().chain() + .required() + .is('notEmpty') + .failure('Framework is required') + ); + + if (form.querySelector('#framework').value.toLowerCase() === 'custom') { + // command is required for custom frameworks + validators.push( + XNAT.validate(form.querySelector('#command')) + .reset().chain() + .required() + .is('notEmpty') + .failure('Command is required') + ); + } else { + // repo, branch and main file path are required for non-custom frameworks + validators.push( + XNAT.validate(form.querySelector('#git-repo-url')) + .reset().chain() + .required() + .is('notEmpty') + .failure('Git Repo URL is required') + ); + + validators.push( + XNAT.validate(form.querySelector('#git-repo-url')) + .reset().chain() + .is('url') + .failure('Git Repo must be a URL') + ) + + validators.push( + XNAT.validate(form.querySelector('#git-repo-branch')) + .reset().chain() + .required() + .is('notEmpty') + .failure('Branch is required') + ); + + validators.push( + XNAT.validate(form.querySelector('#main-file-path')) + .reset().chain() + .required() + .is('notEmpty') + .failure('Main File Path is required') + ); + } + + validators.push( + XNAT.validate(form.querySelector('#jupyter-environment')) + .reset().chain() + .required() + .is('notEmpty') + .failure('Jupyter Environment is required') + ); + + validators.push( + XNAT.validate(form.querySelector('#hardware')) + .reset().chain() + .required() + .is('notEmpty') + .failure('Hardware is required') + ); + + validators.push( + XNAT.validate(form.querySelector('#data-types')) + .reset().chain() + .required() + .is('notEmpty') + .failure('At least one data type is required') + ); + + let errorMessages = []; + + validators.forEach((validator) => { + if (!validator.check()) { + validator.messages.forEach(message => errorMessages.push(message)); + } + }); + + if (errorMessages.length > 0) { + XNAT.dialog.open({ + title: 'Error', + width: 400, + content: '
    • ' + errorMessages.join('
    • ') + '
    ', + }) + return; + } + + (async () => { + + const config = { + id: form.querySelector('#id').value, + dashboard: { + name: form.querySelector('#name').value, + description: form.querySelector('#description').value, + framework: form.querySelector('#framework').value, + command: form.querySelector('#command').value, + fileSource: 'git', + gitRepoUrl: form.querySelector('#git-repo-url').value, + gitRepoBranch: form.querySelector('#git-repo-branch').value, + mainFilePath: form.querySelector('#main-file-path').value, + }, + scopes: { + Site: { + scope: 'Site', + enabled: isEdit ? dashboardConfig?.scopes?.Site?.enabled ?? false : true, + ids: dashboardConfig?.scopes?.Site?.ids ?? [], + }, + Project: { + scope: 'Project', + enabled: false, + ids: isEdit ? dashboardConfig?.scopes?.Project?.ids ?? [] : [], + }, + DataType: { + scope: 'DataType', + enabled: false, + ids: Array.from(form.querySelector('#data-types').selectedOptions).map(o => o.value), + } + }, + computeEnvironmentConfig: { + id: form.querySelector('#jupyter-environment').value, + }, + hardwareConfig: { + id: form.querySelector('#hardware').value, + } + }; + + let response; + if (isNew || isCopy) { + response = XNAT.plugin.jupyterhub.dashboards.configs.create(config); + } else if (isEdit) { + response = XNAT.plugin.jupyterhub.dashboards.configs.update(config); + } + + response.then(() => { + XNAT.ui.banner.top(2000, 'Dashboard saved.', 'success'); + obj.close(); + if (onSaved) { + onSaved(); + } + }).catch((error) => { + XNAT.ui.banner.top(2000, 'Failed to save dashboard', 'error',); + console.error(error); + }); + })(); + } + } + ] + }) + }, + table: async function(querySelector, scope) { + if (!scope) { + console.error('Scope is required'); + return; + } else if (!['Site', 'Project'].includes(scope)) { + console.error(`Invalid scope ${scope}`); + return; + } + + const projectId = XNAT.data.context.project; + + let container, footer; + + const init = (querySelector) => { + container = document.querySelector(querySelector); + container.innerHTML = '
    Loading...
    ' + + container.style.display = 'flex'; + container.style.flexDirection = 'row'; + container.style.justifyContent = 'center'; + + if (scope === 'Site') { + footer = container.closest('.panel').querySelector('.panel-footer'); + footer.innerHTML = ''; + footer.appendChild(newButton()); + } + + refresh(); + + // Refresh table when dashboard framework is saved. Framework names may have changed. + document.addEventListener('dashboard-framework-saved', () => { + refresh(); + }); + } + + const clear = () => { + container.innerHTML = ''; + } + + const newButton = () => { + return spawn('div', [ + spawn('div.pull-right', [ + spawn('button.btn.btn-sm', { html: 'New Dashboard' , onclick: () => XNAT.plugin.jupyterhub.dashboards.configs.editor(null, 'create', refresh)}), + ]), + spawn('div.clear.clearFix') + ]); + } + + const refresh = async () => { + XNAT.plugin.jupyterhub.dashboards.configs.getAll().then((dashboardConfigs) => { + clear(); + + if (dashboardConfigs.length === 0) { + container.innerHTML = `
    No dashboards found
    `; + } else { + return table(dashboardConfigs); + } + }).catch((error) => { + console.error(error); + container.innerHTML = `
    Failed to load dashboards. See console and system logs for details.
    `; + }); + } + + const remove = async (id) => { + XNAT.dialog.open({ + title: 'Confirm', + content: 'Are you sure you want to delete this dashboard?', + width: 400, + buttons: [ + { + label: 'Cancel', + isDefault: false, + close: false, + action: function () { + XNAT.dialog.closeAll(); + } + }, + { + label: 'Delete', + isDefault: true, + close: false, + action: function (obj) { + XNAT.plugin.jupyterhub.dashboards.configs.delete(id).then(() => { + XNAT.ui.banner.top(2000, 'Dashboard deleted.', 'success'); + refresh(); + XNAT.dialog.closeAll(); + }).catch((error) => { + XNAT.ui.banner.top(2000, 'Failed to delete dashboard', 'error',); + console.error(error); + }); + } + } + ] + }) + } + + const enableToggle = (config, scope, id) => { + if (scope === 'Project' && !config?.scopes?.Site?.enabled) { + // If the dashboard is not enabled for the site, it cannot be enabled for a project + return spawn('div.center', ['Disabled for site']); + } + + const scopeActions = { + 'Site': { + getEnabled: () => config?.scopes?.Site?.enabled ?? undefined, + enableAction: () => XNAT.plugin.jupyterhub.dashboards.configs.enableForSite(config['id']), + disableAction: () => XNAT.plugin.jupyterhub.dashboards.configs.disableForSite(config['id']) + }, + 'Project': { + getEnabled: () => config?.scopes?.Project?.ids?.includes(id) ?? false, + enableAction: () => XNAT.plugin.jupyterhub.dashboards.configs.enableForProject(config['id'], id), + disableAction: () => XNAT.plugin.jupyterhub.dashboards.configs.disableForProject(config['id'], id) + } + }; + + let scopeAction = scopeActions[scope]; + + if (!scopeAction) { + console.error(`Invalid scope ${scope}`); + return; + } + + let enabled = scopeAction.getEnabled(); + + let ckbox = spawn('input', { + type: 'checkbox', + checked: enabled, + value: enabled ? 'true' : 'false', + data: { checked: enabled }, + onchange: () => { + let enabled = ckbox.checked; + let action = enabled ? scopeAction.enableAction : scopeAction.disableAction; + + action() + .then(() => { + XNAT.ui.banner.top(2000, `Dashboard ${enabled ? 'enabled' : 'disabled'}.`, 'success'); + }) + .catch((error) => { + XNAT.ui.banner.top(2000, `Failed to ${enabled ? 'enable' : 'disable'} dashboard.`, 'error'); + console.error(error); + toggleCheckbox(!enabled); + }); + } + }); + + let toggleCheckbox = (enabled) => { + ckbox.checked = enabled; + ckbox.value = enabled ? 'true' : 'false'; + ckbox.dataset.checked = enabled; + }; + + return spawn('label.switchbox', [ + ckbox, + ['span.switchbox-outer', [['span.switchbox-inner']]] + ]); + }; + + const table = async (dashboardConfigs) => { + const tableColumns = { + name: { + label: 'Dashboard', + filter: true, + th: { className: 'left' }, + apply: function () { + return spawn('div.left', [ + spawn('span', {}, `${this.dashboard?.name}
    ${this.dashboard?.description}`) + ]); + } + }, + gitRepoUrl: { + label: 'Source', + th: { style: { width: '75px' } }, + apply: function () { + const isGithub = this.dashboard?.gitRepoUrl.includes('github.com'); + + // if no git repo url, show nothing + if (!this.dashboard?.gitRepoUrl || this.dashboard?.gitRepoUrl === '') { + return spawn('div.center', ['']); + } + + // otherwise, show github icon or code icon with link to git repo + return spawn('div.center', [ + spawn('a', { href: this.dashboard?.gitRepoUrl ?? '', target: '_blank', style: { color: 'black' } + }, isGithub ? `` : ``) + ]); + } + }, + dataTypes: { + label: 'Data Types', + th: { style: { width: '150px' } }, + filter: true, + apply: function () { + let display = ''; + + if (this.scopes?.DataType?.enabled) { + display = 'All'; + } else { + // Sort ids alphabetically + let sortedIds = this.scopes?.DataType?.ids?.sort((a, b) => a.localeCompare(b)); + display = sortedIds?.join(', '); + + // if display is empty, show 'None' + if (display === '' || display === ' ') { + display = 'None'; + } + } + + return spawn('div.left', [ + spawn('span', {}, display) + ]); + } + }, + isEnabled: { + label: 'Enabled', + th: { style: { width: '50px' } }, + apply: function() { + return spawn('div.center', [enableToggle(this, scope, projectId)]); + } + }, + actions: { + label: 'Actions', + th: { style: { width: '150px' } }, + apply: function() { + return spawn('div.center', [ + spawn('button.btn.btn-sm', { onclick: () => XNAT.plugin.jupyterhub.dashboards.configs.editor(this, 'edit', refresh) }, ''), + spawn('span', { style: { display: 'inline-block', width: '4px' } }), + spawn('button.btn.btn-sm', { onclick: () => XNAT.plugin.jupyterhub.dashboards.configs.editor(this, 'copy', refresh) }, ''), + spawn('span', { style: { display: 'inline-block', width: '4px' } }), + spawn('button.btn.btn-sm', { onclick: () => remove(this['id'])}, '') + ]); + } + } + }; + + if (scope === 'Project') { + delete tableColumns['actions']; + } + + const table = XNAT.table.dataTable(dashboardConfigs, { + header: true, + sortable: 'name, version, framework, fileSource, computeEnvironment, hardware, isPublic, isEnabled', + columns: tableColumns + }); + + clear() + table.render(`${querySelector}`); + } + + init(querySelector); + + return { + init: init, + refresh: refresh, + } + } + } + + XNAT.plugin.jupyterhub.dashboards.frameworks = { + url: `/xapi/jupyterhub/dashboards/frameworks`, + get: async function (name) { + const url = XNAT.url.csrfUrl(`${this.url}/${name}`); + + const response = await fetch(url, { + method: 'GET', + headers: {'Content-Type': 'application/json'} + }); + + if (!response.ok) { + throw new Error(`HTTP error getting dashboard framework ${name}: ${response.status}`); + } + + return response.json(); + }, + getAll: async function () { + const url = XNAT.url.csrfUrl(this.url); + + const response = await fetch(url, { + method: 'GET', + headers: {'Content-Type': 'application/json'} + }); + + if (!response.ok) { + throw new Error(`HTTP error getting dashboard frameworks: ${response.status}`); + } + + let frameworks = await response.json(); + frameworks.sort((a, b) => a.name.localeCompare(b.name)); + return frameworks; + }, + create: async function (framework) { + const url = XNAT.url.csrfUrl(this.url); + + const response = await fetch(url, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(framework) + }); + + if (!response.ok) { + throw new Error(`HTTP error creating dashboard framework: ${response.status}`); + } + + return response.json(); + }, + update: async function (name, framework) { + name = encodeURIComponent(name); + const url = XNAT.url.csrfUrl(`${this.url}/${name}`); + + const response = await fetch(url, { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(framework) + }); + + if (!response.ok) { + throw new Error(`HTTP error updating dashboard framework ${name}: ${response.status}`); + } + + return response.json(); + }, + delete: async function (name) { + name = encodeURIComponent(name); + const url = XNAT.url.csrfUrl(`${this.url}/${name}`); + + const response = await fetch(url, { + method: 'DELETE', + headers: {'Content-Type': 'application/json'} + }); + + if (!response.ok) { + throw new Error(`HTTP error deleting dashboard framework ${name}: ${response.status}`); + } + }, + editor: async function(framework, action, onSaved) { + let isNew = action === 'create', + isCopy = action === 'copy', + isEdit = action === 'edit', + title = isNew || isCopy ? 'Add Dashboard Framework' : 'Edit Dashboard Framework'; + + XNAT.dialog.open({ + title: title, + content: spawn('div.dashboard-framework-editor'), + width: 750, + maxBtn: true, + beforeShow: () => { + // Create form + const formContainer = document.querySelector(`.dashboard-framework-editor`); + formContainer.classList.add('panel'); + + framework = framework ? framework : {}; + + let id = isNew || isCopy ? '' : framework?.id ?? '', + name = isCopy ? '' : framework?.name ?? '', + commandTemplate = framework?.commandTemplate ?? ''; + + let form = spawn('form.dashboard-framework-edit-form', [ + spawn('style|type=text/css', ` + .panel .panel-element input[type="text"], + .panel .panel-element select, + .panel .panel-element textarea, + .panel .panel-element .description { + width: 400px; + } + + .panel .panel-element select[multiple] { + max-width: 400px; + height: 150px; + } + + code { + white-space: pre-wrap; + } + `), + spawn('input#id', { type: 'hidden', value: id }), + spawn('div.panel-element|data-name=name', [ + spawn('label.element-label|for=name', 'Name'), + spawn('div.element-wrapper', [ + spawn(`input#name|type=text`, { value: name }), + spawn('div.description', 'The name of the dashboard framework') + ]), + spawn('div.clear') + ]), + spawn('div.panel-element|data-name=command-template', [ + spawn('label.element-label|for=command-template', 'Command Template'), + spawn('div.element-wrapper', [ + spawn( + `textarea#command-template|rows=10`, + { style: { fontFamily: 'sans-serif' } }, + commandTemplate + ), + spawn('div.description', + 'Enter a template for the command that will be executed to start the dashboard container. ' + + 'Use the following placeholders to insert values into the command: ' + + '
    • {repo} - The URL of the Git repository
    • {repobranch} - ' + + 'The branch of the Git repository
    • {mainFilePath} - The path to the' + + ' main file in the Git repository
    ' + + 'Use the jh-single-native-proxy python package to proxy the dashboard ' + + 'container through JupyterHub.' + + '

    ' + + 'Example:
    ' + + '' + + 'jhsingle-native-proxy\n' + + '\t--destport 8505\n' + + '\t--repo {repo}\n' + + '\t--repobranch {repobranch}\n' + + '\t--repofolder /home/jovyan/dashboards\n' + + 'streamlit run\n' + + '\t/home/jovyan/dashboards/{mainFilePath}\n' + + '\t{--}server.port 8505\n' + + '\t{--}server.headless True\n' + + '' + + '

    ' + + 'See documentation for more details.' + ) + ]), + spawn('div.clear') + ]), + ]); + + formContainer.appendChild(form); + }, + buttons: [ + { + label: 'Cancel', + isDefault: false, + close: false, + action: function () { + XNAT.dialog.closeAll(); + } + }, + { + label: isNew || isCopy ? 'Add Framework' : 'Save Framework', + isDefault: true, + close: false, + action: function (obj) { + let form = document.querySelector('.dashboard-framework-edit-form'); + + const validators = []; + + validators.push( + XNAT.validate(form.querySelector('#name')) + .reset().chain() + .required() + .is('notEmpty') + .failure('Name is required') + ); + + validators.push( + XNAT.validate(form.querySelector('#command-template')) + .reset().chain() + .required() + .is('notEmpty') + .failure('Command template is required') + ); + + let errorMessages = []; + + validators.forEach((validator) => { + if (!validator.check()) { + validator.messages.forEach(message => errorMessages.push(message)); + } + }); + + if (errorMessages.length > 0) { + XNAT.dialog.open({ + title: 'Error', + width: 400, + content: '
    • ' + errorMessages.join('
    • ') + '
    ', + }) + return; + } + + const frameworkToSave = { + id: form.querySelector('#id').value, + name: form.querySelector('#name').value, + commandTemplate: form.querySelector('#command-template').value + }; + + let response; + if (isNew || isCopy) { + response = XNAT.plugin.jupyterhub.dashboards.frameworks.create(frameworkToSave); + } else if (isEdit) { + response = XNAT.plugin.jupyterhub.dashboards.frameworks.update(framework['name'], frameworkToSave); + } + + response.then(() => { + XNAT.ui.banner.top(2000, 'Dashboard framework saved.', 'success'); + obj.close(); + if (onSaved) { + onSaved(); + } + + // Dispatch event to refresh dashboard configs table + document.dispatchEvent(new Event('dashboard-framework-saved')); + }).catch((error) => { + XNAT.ui.banner.top(2000, 'Failed to save dashboard framework', 'error',); + console.error(error); + }); + } + } + ], + }); + }, + table: async function(querySelector) { + let container, footer; + + const init = (querySelector) => { + container = document.querySelector(querySelector); + container.innerHTML = '
    Loading...
    ' + + container.style.display = 'flex'; + container.style.flexDirection = 'row'; + container.style.justifyContent = 'center'; + + footer = container.closest('.panel').querySelector('.panel-footer'); + footer.innerHTML = ''; + footer.appendChild(newButton()); + + refresh(); + } + + const clear = () => { + container.innerHTML = ''; + } + + const refresh = async () => { + const frameworks = await XNAT.plugin.jupyterhub.dashboards.frameworks.getAll(); + + clear(); + + if (Object.keys(frameworks).length === 0) { + container.innerHTML = `
    No frameworks found
    `; + } else { + return table(frameworks); + } + } + + const newButton = () => { + return spawn('div', [ + spawn('div.pull-right', [ + spawn('button.btn.btn-sm', { html: 'Add Framework' , onclick: () => XNAT.plugin.jupyterhub.dashboards.frameworks.editor(null, 'create', refresh)}), + ]), + spawn('div.clear.clearFix') + ]); + } + + const remove = async (name) => { + XNAT.dialog.open({ + title: 'Confirm', + content: 'Are you sure you want to delete this dashboard framework?', + width: 400, + buttons: [ + { + label: 'Cancel', + isDefault: false, + close: false, + action: function () { + XNAT.dialog.closeAll(); + } + }, + { + label: 'Delete', + isDefault: true, + close: false, + action: function (obj) { + XNAT.plugin.jupyterhub.dashboards.frameworks.delete(name).then(() => { + XNAT.ui.banner.top(2000, 'Dashboard framework deleted.', 'success'); + refresh(); + XNAT.dialog.closeAll(); + }).catch((error) => { + XNAT.ui.banner.top(2000, 'Failed to delete dashboard framework', 'error',); + console.error(error); + }); + } + } + ] + }); + } + + const table = async (frameworks) => { + const tableColumns = { + name: { + label: 'Name', + filter: true, + th: { className: 'left' }, + apply: function () { + return spawn('div.left', [ + spawn('span', {}, this['name']) + ]); + } + }, + actions: { + label: 'Actions', + th: { style: { width: '150px' } }, + apply: function() { + return spawn('div.center', [ + spawn('button.btn.btn-sm', { onclick: () => XNAT.plugin.jupyterhub.dashboards.frameworks.editor(this, 'edit', refresh) }, ''), + spawn('span', { style: { display: 'inline-block', width: '4px' } }), + spawn('button.btn.btn-sm', { onclick: () => XNAT.plugin.jupyterhub.dashboards.frameworks.editor(this, 'copy', refresh) }, ''), + spawn('span', { style: { display: 'inline-block', width: '4px' } }), + spawn('button.btn.btn-sm', { onclick: () => remove(this['name'])}, '') + ]); + } + } + }; + + const table = XNAT.table.dataTable(frameworks, { + header: true, + sortable: 'name', + columns: tableColumns + }); + + clear() + table.render(`${querySelector}`); + } + + init(querySelector); + } + } + +})); \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js index 5c5befd..39c0b97 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-servers.js @@ -11,6 +11,8 @@ XNAT.plugin = getObject(XNAT.plugin || {}); XNAT.plugin.jupyterhub = getObject(XNAT.plugin.jupyterhub || {}); XNAT.plugin.jupyterhub.servers = getObject(XNAT.plugin.jupyterhub.servers || {}); XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.servers.user_options || {}); +XNAT.compute = getObject(XNAT.compute || {}); +XNAT.compute.computeEnvironmentConfigs = getObject(XNAT.compute.computeEnvironmentConfigs || {}); (function(factory) { if (typeof define === 'function' && define.amd) { @@ -79,6 +81,16 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s startServer(username, servername, xsiType, itemId, projectId, projectId, eventTrackingId) } + XNAT.plugin.jupyterhub.servers.startDashboardForProject = function(username = window.username, + servername = XNAT.data.context.projectID, + xsiType = XNAT.data.context.xsiType, + itemId = XNAT.data.context.projectID, + projectId = XNAT.data.context.projectID, + eventTrackingId = generateEventTrackingId()) { + console.debug(`jupyterhub-servers.js: XNAT.plugin.jupyterhub.servers.startDashboardForProject`); + startDashboard(username, servername, xsiType, itemId, projectId, projectId, eventTrackingId); + } + XNAT.plugin.jupyterhub.servers.startServerForSubject = function(username = window.username, servername = XNAT.data.context.ID, xsiType = XNAT.data.context.xsiType, @@ -109,6 +121,39 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s } } + XNAT.plugin.jupyterhub.servers.startDashboardForSubject = function(username = window.username, + servername = XNAT.data.context.ID, + xsiType = XNAT.data.context.xsiType, + itemId = XNAT.data.context.ID, + projectId = XNAT.data.context.projectID, + eventTrackingId = generateEventTrackingId()) { + console.debug(`jupyterhub-servers.js: XNAT.plugin.jupyterhub.servers.startServerForSubject`); + + if (XNAT.plugin.jupyterhub.isSharedItem()) { + XNAT.dialog.open({ + width: 450, + title: "Unsupported Operation", + content: "Cannot start a dashboard on a shared subject.", + buttons: [ + { + label: 'OK', + isDefault: true, + close: true, + action: function() { + xmodal.closeAll(); + } + } + ] + }); + } else { + getSubjectLabel(itemId).then(subjectLabel => { + startDashboard(username, servername, xsiType, itemId, subjectLabel, projectId, eventTrackingId) + }).catch(e => { + console.error(`Error getting subject label: ${e}`); + }); + } + } + XNAT.plugin.jupyterhub.servers.startServerForExperiment = function(username = window.username, servername = XNAT.data.context.ID, xsiType = "xnat:experimentData", @@ -139,6 +184,39 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s } } + XNAT.plugin.jupyterhub.servers.startDashboardForExperiment = function(username = window.username, + servername = XNAT.data.context.ID, + xsiType = XNAT.data.context.xsiType, + itemId = XNAT.data.context.ID, + projectId = XNAT.data.context.projectID, + eventTrackingId = generateEventTrackingId()) { + console.debug(`jupyterhub-servers.js: XNAT.plugin.jupyterhub.servers.startDashboardForExperiment`); + + if (XNAT.plugin.jupyterhub.isSharedItem()) { + XNAT.dialog.open({ + width: 450, + title: "Unsupported Operation", + content: "Cannot start a dashboard on a shared experiment.", + buttons: [ + { + label: 'OK', + isDefault: true, + close: true, + action: function() { + xmodal.closeAll(); + } + } + ] + }); + } else { + getExperimentLabel(itemId).then(experimentLabel => { + startDashboard(username, servername, xsiType, itemId, experimentLabel, projectId, eventTrackingId) + }).catch(e => { + console.error(`Error getting experiment label: ${e}`); + }); + } + } + XNAT.plugin.jupyterhub.servers.startServerForStoredSearch = function(username, servername, xsiType, itemId, itemLabel, projectId, eventTrackingId) { console.debug(`jupyterhub-servers.js: XNAT.plugin.jupyterhub.servers.startServerForStoredSearch`); @@ -251,7 +329,7 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s beforeSend: function () { XNAT.app.activityTab.start( 'Start Jupyter Notebook Server' + - `
    `, + `
    `, eventTrackingId, 'XNAT.plugin.jupyterhub.servers.activityTabCallback', 1000); }, @@ -374,6 +452,195 @@ XNAT.plugin.jupyterhub.servers.user_options = getObject(XNAT.plugin.jupyterhub.s }); } + let startDashboard = XNAT.plugin.jupyterhub.servers.startDashboard = function(username, servername, xsiType, itemId, itemLabel, projectId, eventTrackingId) { + console.debug(`jupyterhub-servers.js: XNAT.plugin.jupyterhub.servers.startDashboard`); + console.debug(`Starting dashboard. User: ${username}, Server Name: ${servername}, XSI Type: ${xsiType}, ID: ${itemId}, Label: ${itemLabel}, Project ID: ${projectId}, eventTrackingId: ${eventTrackingId}`); + + const executionScope = { + 'site': 'XNAT', + 'user': username, + 'prj': projectId, + 'datatype': xsiType, + } + + XNAT.plugin.jupyterhub.dashboards.configs.available(executionScope).then(dashboardConfigs => { + const cancelButton = { + label: 'Cancel', + isDefault: false, + close: true, + } + + const startButton = { + label: 'Start Dashboard', + isDefault: true, + close: false, + action: function(obj) { + const dashboardConfigId = document.querySelector('#dashboard-config').value; + const computeEnvironmentConfigId = document.querySelector('#compute-environment-config').value; + const hardwareConfigId = document.querySelector('#hardware-config').value; + + if (!dashboardConfigId) { + XNAT.dialog.open({ + width: 450, + title: "Error", + content: "Please select a Dashboard.", + buttons: [ + { + label: 'OK', + isDefault: true, + close: true, + } + ] + }); + + return; + } + + const serverStartRequest = { + 'username': username, + 'servername': '', // Not supported yet + 'xsiType': xsiType, + 'itemId': itemId, + 'itemLabel': itemLabel, + 'projectId': projectId, + 'eventTrackingId': eventTrackingId, + 'dashboardConfigId': dashboardConfigId, + 'computeEnvironmentConfigId': computeEnvironmentConfigId, + 'hardwareConfigId': hardwareConfigId, + } + + XNAT.xhr.ajax({ + url: newServerUrl(username, servername), + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(serverStartRequest), + beforeSend: function () { + XNAT.app.activityTab.start( + 'Start Dashboard' + + `
    `, + eventTrackingId, + 'XNAT.plugin.jupyterhub.servers.activityTabCallback', 1000); + }, + fail: function (error) { + console.error(`Failed to send Jupyter server request: ${error}`) + } + }); + + xmodal.closeAll(); + XNAT.ui.dialog.closeAll(); + } + } + + const buttons = (dashboardConfigs.length === 0) ? [cancelButton] : [cancelButton, startButton]; + + XNAT.dialog.open({ + title: 'Start Dashboard', + content: spawn('form#server-start-request-form'), + maxBtn: true, + width: 400, + height: 500, + beforeShow: function(obj) { + const form = document.getElementById('server-start-request-form'); + form.classList.add('panel'); + + if (dashboardConfigs.length === 0) { + obj.$modal.find('.xnat-dialog-content').html('
    No dashboards are available. Please contact your administrator.
    '); + return; + } + + let xnatData = spawn('div.xnat-data', { + style: { + marginTop: '10px', + marginBottom: '40px', + } + }, [ + spawn('h2', 'XNAT Data'), + spawn('p.xnat-data-row.description', 'The following data will be available to your dashboard.'), + spawn('p.xnat-data-row.project', [ + spawn('strong', 'Project '), projectId, + ]), + spawn('p.xnat-data-row.subject', [ + spawn('strong', 'Subject '), itemLabel, + ]), + spawn('p.xnat-data-row.experiment', [ + spawn('strong', 'Experiment '), itemLabel, + ]), + ]); + + xnatData.querySelectorAll('strong').forEach(s => { + s.style.display = 'inline-block'; + s.style.width = '90px'; + }); + + if (xsiType === 'xnat:projectData') { + xnatData.querySelector('p.subject').remove(); + xnatData.querySelector('p.experiment').remove(); + } else if (xsiType === 'xnat:subjectData') { + xnatData.querySelector('p.project').remove(); + xnatData.querySelector('p.experiment').remove(); + } else { // xsiType is an xnat:experimentData + xnatData.querySelector('p.project').remove(); + xnatData.querySelector('p.subject').remove(); + } + + let dashboardConfigSelect = spawn('div', [ + spawn('h2', 'Dashboard'), + spawn('p.description', 'Select from the list of available dashboards.'), + spawn('div.form-group', [ + spawn('select#dashboard-config | size=5', [ + ...dashboardConfigs.map(c => spawn('option', {value: c['id']}, c['dashboard']['name'])), + ]), + spawn('div#dashboard-description.description', ''), + ]), + spawn('input#hardware-config', {type: 'hidden', value: ''}), + spawn('input#compute-environment-config', {type: 'hidden', value: ''}), + spawn('style', {type: 'text/css'}, ` + + div.form-group { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + gap: 10px; + } + + div.form-group select { + width: 100%; + font-size: 1.1em; + } + + div.form-group div.description { + font-size: 1em; + color: #555; + font-weight: bold; + } + + p.description { + font-size: .9em; + color: #777; + } + + `) + ]); + + form.appendChild(spawn('!', [ + xnatData, + dashboardConfigSelect + ])); + + dashboardConfigSelect.querySelector('select').addEventListener('change', () => { + const dashboardConfig = dashboardConfigs.filter(c => c['id'].toString() === dashboardConfigSelect.querySelector('select').value)[0]; + const dashboard = dashboardConfig['dashboard']; + document.getElementById('hardware-config').value = dashboardConfig['hardwareConfig']['id']; + document.getElementById('compute-environment-config').value = dashboardConfig['computeEnvironmentConfig']['id']; + document.getElementById('dashboard-description').innerHTML = dashboard['description']; + }); + }, + buttons: buttons + }); + }) + } + let activityTabCallback = XNAT.plugin.jupyterhub.servers.activityTabCallback = function(itemDivId, detailsTag, jsonobj, lastProgressIdx) { const succeeded = jsonobj['succeeded']; const payload = JSON.parse(jsonobj['payload']); diff --git a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-topnav.js b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-topnav.js index a01cda8..625e818 100644 --- a/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-topnav.js +++ b/src/main/resources/META-INF/resources/scripts/xnat/plugin/jupyterhub/jupyterhub-topnav.js @@ -37,9 +37,15 @@ XNAT.plugin.jupyterhub.topnav = getObject(XNAT.plugin.jupyterhub.topnav || {}); // add table header row jupyterServerTable.tr() + .th({addClass: 'left', style: {width: '75px'}, html: 'Application'}) .th({addClass: 'left', style: {width: '200px'}, html: 'Context'}) .th({addClass: 'center', style: {width: '100px'}, html: 'Actions'}) + function application(server) { + return server['user_options']['dashboardConfigId'] != null && + server['user_options']['dashboardConfigId'] !== '' ? 'Dashboard' : 'Notebook'; + } + function xnatItem(server) { const userOptions = server['user_options']; const xsiType = userOptions['xsiType']; @@ -60,12 +66,6 @@ XNAT.plugin.jupyterhub.topnav = getObject(XNAT.plugin.jupyterhub.topnav || {}); title: itemLabel, html: itemLabel, }); - case 'xnat:experimentData': - return spawn('a', { - href: restUrl(`/data/experiments/${itemId}?format=html`), - title: itemLabel, - html: itemLabel, - }); case 'xdat:stored_search': if (itemId.startsWith('@')) { if (projectId == null) {// Site wide data bundle @@ -95,18 +95,29 @@ XNAT.plugin.jupyterhub.topnav = getObject(XNAT.plugin.jupyterhub.topnav || {}); html: itemLabel, }); } - } + // If not project, subject, or stored search, default to experiment + case 'xnat:experimentData': + default: + return spawn('a', { + href: restUrl(`/data/experiments/${itemId}?format=html`), + title: itemLabel, + html: itemLabel, + }); } } + function isDashboard(server) { + return server?.user_options?.dashboardConfigId != null && server?.user_options?.dashboardConfigId !== ''; + } + function gotoServerButton(server) { return spawn('button.btn.sm', { onclick: function(e) { e.preventDefault(); XNAT.plugin.jupyterhub.servers.goTo(server['url']) } - }, [ spawn('i.fa.fa-book|title="Go to Jupyter notebook server"') ]) + }, [ spawn(`i.fa.fa-external-link|title="Go to ${isDashboard(server) ? 'dashboard' : 'Jupyter Notebook Server'}"`) ]) } function stopServerButton(server) { @@ -117,24 +128,24 @@ XNAT.plugin.jupyterhub.topnav = getObject(XNAT.plugin.jupyterhub.topnav || {}); height: 220, scroll: false, content: "" + - "

    Are you sure you'd like to stop this Jupyer notebook server?

    " + + `

    Are you sure you'd like to stop this ${isDashboard(server) ? 'dashboard' : 'Jupyter Notebook Server'}?

    ` + "

    This action cannot be undone.

    ", okAction: function() { const eventTrackingId = XNAT.plugin.jupyterhub.servers.generateEventTrackingId() XNAT.plugin.jupyterhub.servers.stopServer(window.username, server['name'], eventTrackingId).then(() => { XNAT.app.activityTab.start( - 'Stop Jupyter Notebook Server', + `Stop ${isDashboard(server) ? 'Dashboard' : 'Jupyter Notebook Server'}`, eventTrackingId, 'XNAT.plugin.jupyterhub.servers.activityTabCallback', 2000); }).catch(error => { console.error(error); - XNAT.dialog.alert(`Failed to stop Jupyter server: ${error}`) + XNAT.dialog.alert(`Failed to stop ${isDashboard(server) ? 'dashboard' : 'Jupyter Notebook Server'}.`); }); } }) } - }, [ spawn('i.fa.fa-ban|title="Stop Jupyter notebook server"') ]) + }, [ spawn(`i.fa.fa-ban|title="Stop ${isDashboard(server) ? 'Dashboard' : 'Jupyter Notebook Server'}"`) ]) } function spacer(width = 10) { @@ -153,17 +164,17 @@ XNAT.plugin.jupyterhub.topnav = getObject(XNAT.plugin.jupyterhub.topnav || {}); isEmpty(servers) ? jupyterServerTable.tr() - .td([ spawn('div.left', {style: {'font-size': '12px'}}, ['No running Jupyter servers. Go to a project, subject, or experiment to start Jupyter.']) ]) - .td([ spawn('div.center', ['']) ]) : + .td({colSpan: '3'}, [ spawn('div.left', {style: {'font-size': '12px'}}, ['No running Jupyter notebooks or dashboards. Go to a project, subject, or experiment to start one.']) ]): Object.values(servers).forEach(server => { jupyterServerTable.tr() + .td([ spawn('div.left', {style: {'font-size': '12px', 'font-weight': 'bold'}}, [application(server)]) ]) .td([ spawn('div.left', [xnatItem(server)]) ]) .td([ spawn('div.center', [gotoServerButton(server), spacer(), stopServerButton(server)]) ]) }); }).catch(() => { jupyterServerTable.tr() - .td([ spawn('div.left', {style: {'font-size': '12px'}}, ['Unable to connect to JupyterHub']) ]) + .td({colSpan: '3'}, [ spawn('div.left', {style: {'font-size': '12px'}}, ['Unable to connect to JupyterHub']) ]) .td([ spawn('div.center', ['']) ]); }) diff --git a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter.vm b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter.vm index 1d2c353..0b678a7 100644 --- a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter.vm +++ b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter.vm @@ -1,12 +1,6 @@ -
  • +
  • Jupyter
      #addGlobalCustomScreens("topBar/Jupyter")
  • - - \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm index 3cd9552..a0e94fd 100644 --- a/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm +++ b/src/main/resources/META-INF/resources/templates/screens/topBar/Jupyter/Default.vm @@ -1,5 +1,5 @@ -
  • Jupyter Activity +
  • Activity
    @@ -13,4 +13,5 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/screens/xnat_experimentData/actionsBox/StartJupyterServer.vm b/src/main/resources/META-INF/resources/templates/screens/xnat_experimentData/actionsBox/StartJupyterServer.vm new file mode 100644 index 0000000..1940a98 --- /dev/null +++ b/src/main/resources/META-INF/resources/templates/screens/xnat_experimentData/actionsBox/StartJupyterServer.vm @@ -0,0 +1,31 @@ +
  • + +
  • + +
  • + +
  • + + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/screens/xnat_imageSessionData/actionsBox/StartJupyterServer.vm b/src/main/resources/META-INF/resources/templates/screens/xnat_imageSessionData/actionsBox/StartJupyterServer.vm index 29ff729..1940a98 100644 --- a/src/main/resources/META-INF/resources/templates/screens/xnat_imageSessionData/actionsBox/StartJupyterServer.vm +++ b/src/main/resources/META-INF/resources/templates/screens/xnat_imageSessionData/actionsBox/StartJupyterServer.vm @@ -4,8 +4,28 @@ +
  • + +
  • + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/screens/xnat_projectData/actionsBox/StartJupyterServer.vm b/src/main/resources/META-INF/resources/templates/screens/xnat_projectData/actionsBox/StartJupyterServer.vm index c87f6c8..f579dc6 100644 --- a/src/main/resources/META-INF/resources/templates/screens/xnat_projectData/actionsBox/StartJupyterServer.vm +++ b/src/main/resources/META-INF/resources/templates/screens/xnat_projectData/actionsBox/StartJupyterServer.vm @@ -4,8 +4,28 @@ +
  • + +
  • + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/screens/xnat_subjectData/actionsBox/StartJupyterServer.vm b/src/main/resources/META-INF/resources/templates/screens/xnat_subjectData/actionsBox/StartJupyterServer.vm index 2b3cf1b..2a21fa3 100644 --- a/src/main/resources/META-INF/resources/templates/screens/xnat_subjectData/actionsBox/StartJupyterServer.vm +++ b/src/main/resources/META-INF/resources/templates/screens/xnat_subjectData/actionsBox/StartJupyterServer.vm @@ -4,8 +4,28 @@ +
  • + +
  • + \ No newline at end of file diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/project-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/project-settings.yaml new file mode 100644 index 0000000..e76fb36 --- /dev/null +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/project-settings.yaml @@ -0,0 +1,41 @@ +dashboardsPrj: + kind: panel + name: dashboards + label: Dashboards + contents: + description: + tag: div.message + element: + style: "margin-bottom: 20px;" + contents: + "Dashboards are a way to deploy Jupyter notebooks (and other tools) as interactive web applications. Enable the dashboards you want to use in this project." + dashboardConfigsTable: + tag: "div#jupyterhub-dashboards-table" + dashboardScripts: + tag: script|src="~/scripts/xnat/plugin/jupyterhub/jupyterhub-dashboards.js" + renderDashboardConfigsTable: + tag: script + content: > + XNAT.plugin.jupyterhub.dashboards.configs.table('div#jupyterhub-dashboards-table', 'Project'); + +####################################################### +#### Root Project Settings Spawner Config Object #### +####################################################### + +projectSettings: + kind: tabs + name: jupyterhubProjectSettings + label: JupyterHub + meta: + tabGroups: + jupyterhubTabGroupPrj: JupyterHub + contains: tabs + tabs: + dashboardSettingsTabPrj: + kind: tab + name: dashboardSettingsTabPrj + label: Dashboards + group: jupyterhubTabGroupPrj + active: true + contents: + ${dashboardsPrj} \ No newline at end of file diff --git a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml index ac084a0..3f13ae9 100644 --- a/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml +++ b/src/main/resources/META-INF/xnat/spawner/jupyterhub/site-settings.yaml @@ -313,6 +313,58 @@ jupyterHubUserActivity: content: > XNAT.plugin.jupyterhub.users.activity.init('jupyterhub-user-activity-table'); +dashboards: + kind: panel + name: dashboards + label: Dashboards + contents: + description: + tag: div.message + element: + style: "margin-bottom: 20px;" + contents: + "Dashboards are a way to deploy Jupyter notebooks (and other tools) as interactive web applications using JupyterHub. + Create a configuration for each dashboard you want to deploy. Dashboards are configured to launch in + specific Jupyter environments and are associated with a specific Hardware configuration. You can also + specify which data types are supported by the dashboard. After enabling a dashboard, project owners can then + enable the dashboard for their projects. See the documentation for more information." + dashboardConfigsTable: + tag: "div#jupyterhub-dashboards-table" + dashboardScripts: + tag: script|src="~/scripts/xnat/plugin/jupyterhub/jupyterhub-dashboards.js" + computeEnvironmentConfigsScript: + tag: script|src="~/scripts/xnat/compute/compute-environment-configs.js" + hardwareConfigsScript: + tag: script|src="~/scripts/xnat/compute/hardware-configs.js" + renderDashboardConfigsTable: + tag: script + content: > + XNAT.plugin.jupyterhub.dashboards.configs.table('div#jupyterhub-dashboards-table', 'Site'); + +frameworks: + kind: panel + name: frameworks + label: Dashboard Frameworks + contents: + description: + tag: div.message + element: + style: "margin-bottom: 20px;" + contents: + "Configure the frameworks that can be used to deploy dashboards." + dashboardConfigsTable: + tag: "div#dashboard-frameworks-table" + dashboardScripts: + tag: script|src="~/scripts/xnat/plugin/jupyterhub/jupyterhub-dashboards.js" + computeEnvironmentConfigsScript: + tag: script|src="~/scripts/xnat/compute/compute-environment-configs.js" + hardwareConfigsScript: + tag: script|src="~/scripts/xnat/compute/hardware-configs.js" + renderDashboardConfigsTable: + tag: script + content: > + XNAT.plugin.jupyterhub.dashboards.frameworks.table('div#dashboard-frameworks-table'); + ################################################# #### Root Site Admin Spawner Config Object #### ################################################# @@ -338,6 +390,15 @@ siteSettings: ${jupyterEnvironments} ${hardwareConfigs} ${constraints} + jupyterhubDashboardsTab: + kind: tab + name: jupyterhubDashboardsTab + label: Dashboards + group: jupyterhubTabGroup + active: true + contents: + ${dashboards} + ${frameworks} jupyterhubUserActivityTab: kind: tab name: jupyterhubUserActivityTab @@ -345,4 +406,5 @@ siteSettings: group: jupyterhubTabGroup active: true contents: - ${jupyterHubUserActivity} \ No newline at end of file + ${jupyterHubUserActivity} + diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java deleted file mode 100644 index d12f669..0000000 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/authorization/JupyterUserAuthorizationTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.authorization; - -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.nrg.xdat.security.services.RoleServiceI; -import org.nrg.xft.security.UserI; -import org.nrg.xnatx.plugins.jupyterhub.config.JupyterAuthorizationConfig; -import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = JupyterAuthorizationConfig.class) -public class JupyterUserAuthorizationTest { - - @Autowired private JupyterUserAuthorization jupyterUserAuthorization; - @Autowired private RoleServiceI mockRoleService; - @Autowired private JupyterHubPreferences mockJupyterHubPreferences; - - private UserI user; - - @Before - public void before() { - // Mock the user - user = mock(UserI.class); - String username = "user"; - when(user.getUsername()).thenReturn(username); - } - - @After - public void after() { - Mockito.reset(mockRoleService, mockJupyterHubPreferences); - } - - @Test - @Ignore - public void testCheckImpl() { - // Tough to test this. super.checkImpl(...) is protected so can't spy with Mockito. - // If you decouple JupyterUserAuthorization from UserXapiAuthorization then you have to deal with - // the JoinPoint in checkImpl(...) - } - - @Test - public void testJupyter_AllUsers() { - // Setup - when(mockJupyterHubPreferences.getAllUsersCanStartJupyter()).thenReturn(true); - when(mockRoleService.checkRole(any(), eq("Jupyter"))).thenReturn(false); - when(mockRoleService.isSiteAdmin(user)).thenReturn(false); - - // Test - boolean check = jupyterUserAuthorization.checkJupyter(user); - - // Verify - assertTrue("Jupyter All User preference is enabled. This should allow all.", check); - } - - @Test - public void testJupyter_JupyterUsers() { - // Setup - when(mockJupyterHubPreferences.getAllUsersCanStartJupyter()).thenReturn(false); - when(mockRoleService.checkRole(any(), eq("Jupyter"))).thenReturn(true); - when(mockRoleService.isSiteAdmin(user)).thenReturn(false); - - // Test - boolean check = jupyterUserAuthorization.checkJupyter(user); - - // Verify - assertTrue("Jupyter role is enabled for the user. This should allow access.", check); - } - - @Test - public void testJupyter_Admin() { - // Setup - when(mockJupyterHubPreferences.getAllUsersCanStartJupyter()).thenReturn(false); - when(mockRoleService.checkRole(any(), eq("Jupyter"))).thenReturn(false); - when(mockRoleService.isSiteAdmin(user)).thenReturn(true); - - // Test - boolean check = jupyterUserAuthorization.checkJupyter(user); - - // Verify - assertFalse("Admin users are not allowed to start Jupyter. They should have the juptyer role.", check); - } - - @Test - public void testGuestUserNeverAuthorized() { - boolean check = jupyterUserAuthorization.considerGuests(); - assertFalse("Guest users should not be authorized", check); - } - -} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DashboardFrameworkInitializerTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DashboardFrameworkInitializerTestConfig.java new file mode 100644 index 0000000..a719b3f --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DashboardFrameworkInitializerTestConfig.java @@ -0,0 +1,26 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.initialize.DashboardFrameworkInitializer; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({MockConfig.class}) +public class DashboardFrameworkInitializerTestConfig { + + @Bean + public DashboardFrameworkInitializer dashboardFrameworkInitializer(final XFTManagerHelper mockXFTManagerHelper, + final XnatAppInfo mockXnatAppInfo, + final DashboardFrameworkService mockDashboardFrameworkService) { + return new DashboardFrameworkInitializer( + mockXFTManagerHelper, + mockXnatAppInfo, + mockDashboardFrameworkService + ); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardConfigServiceTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardConfigServiceTestConfig.java new file mode 100644 index 0000000..c460da6 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardConfigServiceTestConfig.java @@ -0,0 +1,62 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.nrg.xnatx.plugins.jupyterhub.services.impl.DashboardComputeEnvironmentConfigService; +import org.nrg.xnatx.plugins.jupyterhub.services.impl.DashboardHardwareConfigService; +import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultDashboardConfigService; +import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultDashboardFrameworkService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({HibernateEntityServicesConfig.class}) +public class DefaultDashboardConfigServiceTestConfig { + + @Bean + public DefaultDashboardConfigService defaultDashboardConfigService(final DashboardConfigEntityService dashboardConfigEntityService, + final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService, + final HardwareConfigEntityService hardwareConfigEntityService, + final DashboardFrameworkEntityService dashboardFrameworkEntityService) { + return new DefaultDashboardConfigService( + dashboardConfigEntityService, + computeEnvironmentConfigEntityService, + hardwareConfigEntityService, + dashboardFrameworkEntityService + ); + } + + @Bean + public ComputeEnvironmentConfigService defaultComputeEnvironmentConfigService(final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService, + final HardwareConfigEntityService hardwareConfigEntityService, + final DashboardConfigEntityService dashboardConfigEntityService) { + return new DashboardComputeEnvironmentConfigService( + computeEnvironmentConfigEntityService, + hardwareConfigEntityService, + dashboardConfigEntityService + ); + } + + @Bean + public HardwareConfigService defaultHardwareConfigService(final HardwareConfigEntityService hardwareConfigEntityService, + final ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService, + final DashboardConfigEntityService dashboardConfigEntityService) { + return new DashboardHardwareConfigService( + hardwareConfigEntityService, + computeEnvironmentConfigEntityService, + dashboardConfigEntityService + ); + } + + @Bean + public DashboardFrameworkService defaultDashboardFrameworkService(final DashboardFrameworkEntityService dashboardFrameworkEntityService) { + return new DefaultDashboardFrameworkService(dashboardFrameworkEntityService); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardFrameworkServiceTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardFrameworkServiceTestConfig.java new file mode 100644 index 0000000..852b0d8 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardFrameworkServiceTestConfig.java @@ -0,0 +1,20 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultDashboardFrameworkService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({HibernateEntityServicesConfig.class}) +public class DefaultDashboardFrameworkServiceTestConfig { + + @Bean + public DefaultDashboardFrameworkService defaultDashboardFrameworkService(final DashboardFrameworkEntityService dashboardFrameworkEntityService) { + return new DefaultDashboardFrameworkService( + dashboardFrameworkEntityService + ); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardJobTemplateServiceTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardJobTemplateServiceTestConfig.java new file mode 100644 index 0000000..6ff3747 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultDashboardJobTemplateServiceTestConfig.java @@ -0,0 +1,32 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; +import org.nrg.xnat.compute.services.ConstraintConfigService; +import org.nrg.xnat.compute.services.HardwareConfigService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultDashboardJobTemplateService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({MockConfig.class}) +public class DefaultDashboardJobTemplateServiceTestConfig { + + @Bean + public DefaultDashboardJobTemplateService defaultDashboardJobTemplateService(final ComputeEnvironmentConfigService mockComputeEnvironmentConfigService, + final HardwareConfigService mockHardwareConfigService, + final ConstraintConfigService mockConstraintConfigService, + final DashboardConfigService mockDashboardConfigService, + final DashboardFrameworkService mockDashboardFrameworkService) { + return new DefaultDashboardJobTemplateService( + mockComputeEnvironmentConfigService, + mockHardwareConfigService, + mockConstraintConfigService, + mockDashboardConfigService, + mockDashboardFrameworkService + ); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java index 0a4e9eb..47a5e52 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultJupyterHubServiceConfig.java @@ -5,6 +5,7 @@ import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xnatx.plugins.jupyterhub.client.JupyterHubClient; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardJobTemplateService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultJupyterHubService; import org.nrg.xnatx.plugins.jupyterhub.utils.JupyterHubServiceAccountHelper; @@ -25,6 +26,7 @@ public DefaultJupyterHubService defaultJupyterHubService(final JupyterHubClient final JupyterHubPreferences mockJupyterHubPreferences, final UserManagementServiceI mockUserManagementService, final JobTemplateService mockJobTemplateService, + final DashboardJobTemplateService mockDashboardJobTemplateService, final JupyterHubServiceAccountHelper mockJupyterHubServiceAccountHelper) { return new DefaultJupyterHubService(mockJupyterHubClient, mockNrgEventService, @@ -33,6 +35,7 @@ public DefaultJupyterHubService defaultJupyterHubService(final JupyterHubClient mockJupyterHubPreferences, mockUserManagementService, mockJobTemplateService, + mockDashboardJobTemplateService, mockJupyterHubServiceAccountHelper); } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java index bc1b664..ca2208a 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/DefaultUserOptionsServiceConfig.java @@ -5,6 +5,7 @@ import org.nrg.xdat.security.services.SearchHelperServiceI; import org.nrg.xdat.services.AliasTokenService; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardJobTemplateService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsEntityService; import org.nrg.xnatx.plugins.jupyterhub.services.UserWorkspaceService; import org.nrg.xnatx.plugins.jupyterhub.services.impl.DefaultUserOptionsService; @@ -25,7 +26,8 @@ public DefaultUserOptionsService defaultUserOptionsService(final JupyterHubPrefe final SiteConfigPreferences mockSiteConfigPreferences, final UserOptionsEntityService mockUserOptionsEntityService, final PermissionsHelper mockPermissionsHelper, - final JobTemplateService mockJobTemplateService) { + final JobTemplateService mockJobTemplateService, + final DashboardJobTemplateService mockDashboardJobTemplateService) { return new DefaultUserOptionsService(mockJupyterHubPreferences, mockUserWorkspaceService, mockSearchHelperService, @@ -33,7 +35,8 @@ public DefaultUserOptionsService defaultUserOptionsService(final JupyterHubPrefe mockSiteConfigPreferences, mockUserOptionsEntityService, mockPermissionsHelper, - mockJobTemplateService); + mockJobTemplateService, + mockDashboardJobTemplateService); } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateConfig.java new file mode 100644 index 0000000..b75ca44 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateConfig.java @@ -0,0 +1,86 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.hibernate.SessionFactory; +import org.nrg.xnat.compute.entities.*; +import org.nrg.xnatx.plugins.jupyterhub.entities.*; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.hibernate4.HibernateTransactionManager; +import org.springframework.orm.hibernate4.LocalSessionFactoryBean; +import org.springframework.transaction.support.ResourceTransactionManager; + +import javax.sql.DataSource; +import java.io.IOException; +import java.util.Properties; + +@Configuration +public class HibernateConfig { + + @Bean + public Properties hibernateProperties() throws IOException { + Properties properties = new Properties(); + + // Use HSQLDialect instead of H2Dialect to work around issue + // with h2 version 1.4.200 in hibernate < 5.4 (or so) + // where the generated statements to drop tables between tests can't be executed + // as they do not cascade. + // See https://hibernate.atlassian.net/browse/HHH-13711 + // Solution from https://github.com/hibernate/hibernate-orm/pull/3093#issuecomment-562752874 + properties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); + properties.put("hibernate.hbm2ddl.auto", "create"); + properties.put("hibernate.cache.use_second_level_cache", false); + properties.put("hibernate.cache.use_query_cache", false); + + PropertiesFactoryBean hibernate = new PropertiesFactoryBean(); + hibernate.setProperties(properties); + hibernate.afterPropertiesSet(); + return hibernate.getObject(); + } + + @Bean + public DataSource dataSource() { + BasicDataSource basicDataSource = new BasicDataSource(); + basicDataSource.setDriverClassName(org.h2.Driver.class.getName()); + basicDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); + basicDataSource.setUsername("sa"); + return basicDataSource; + } + + @Bean + public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qualifier("hibernateProperties") final Properties properties) { + final LocalSessionFactoryBean bean = new LocalSessionFactoryBean(); + bean.setDataSource(dataSource); + bean.setHibernateProperties(properties); + bean.setAnnotatedClasses( + DashboardEntity.class, + DashboardConfigEntity.class, + DashboardFrameworkEntity.class, + DashboardScopeEntity.class, + UserOptionsEntity.class, + ConstraintConfigEntity.class, + ConstraintEntity.class, + ConstraintScopeEntity.class, + ComputeEnvironmentConfigEntity.class, + ComputeEnvironmentEntity.class, + ComputeEnvironmentScopeEntity.class, + ComputeEnvironmentHardwareOptionsEntity.class, + HardwareConfigEntity.class, + HardwareEntity.class, + HardwareScopeEntity.class, + HardwareConstraintEntity.class, + EnvironmentVariableEntity.class, + MountEntity.class, + GenericResourceEntity.class + ); + return bean; + } + + @Bean + public ResourceTransactionManager transactionManager(final SessionFactory sessionFactory) throws Exception { + return new HibernateTransactionManager(sessionFactory); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateDashboardFrameworkEntityServiceTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateDashboardFrameworkEntityServiceTestConfig.java new file mode 100644 index 0000000..dbc0296 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateDashboardFrameworkEntityServiceTestConfig.java @@ -0,0 +1,26 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.hibernate.SessionFactory; +import org.nrg.xnatx.plugins.jupyterhub.repositories.DashboardFrameworkEntityDao; +import org.nrg.xnatx.plugins.jupyterhub.services.impl.HibernateDashboardFrameworkEntityService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({HibernateConfig.class}) +public class HibernateDashboardFrameworkEntityServiceTestConfig { + + @Bean + public HibernateDashboardFrameworkEntityService hibernateDashboardFrameworkEntityService(@Qualifier("dashboardFrameworkEntityDaoImpl") DashboardFrameworkEntityDao dao) { + return new HibernateDashboardFrameworkEntityService(dao); + } + + @Bean + @Qualifier("dashboardFrameworkEntityDaoImpl") + public DashboardFrameworkEntityDao dashboardFrameworkEntityDao(final SessionFactory sessionFactory) { + return new DashboardFrameworkEntityDao(sessionFactory); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateEntityServicesConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateEntityServicesConfig.java new file mode 100644 index 0000000..bdb783e --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/HibernateEntityServicesConfig.java @@ -0,0 +1,94 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.hibernate.SessionFactory; +import org.nrg.xnat.compute.repositories.ComputeEnvironmentConfigDao; +import org.nrg.xnat.compute.repositories.ConstraintConfigDao; +import org.nrg.xnat.compute.repositories.HardwareConfigDao; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; +import org.nrg.xnat.compute.services.ConstraintConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.services.impl.HibernateComputeEnvironmentConfigEntityService; +import org.nrg.xnat.compute.services.impl.HibernateConstraintConfigEntityService; +import org.nrg.xnat.compute.services.impl.HibernateHardwareConfigEntityService; +import org.nrg.xnatx.plugins.jupyterhub.repositories.DashboardConfigDao; +import org.nrg.xnatx.plugins.jupyterhub.repositories.DashboardFrameworkEntityDao; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.impl.HibernateDashboardConfigEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.impl.HibernateDashboardFrameworkEntityService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({HibernateConfig.class}) +public class HibernateEntityServicesConfig { + + @Bean + public DashboardFrameworkEntityDao dashboardFrameworkEntityDao(final SessionFactory sessionFactory) { + return new DashboardFrameworkEntityDao(sessionFactory); + } + + @Bean + public DashboardFrameworkEntityService dashboardFrameworkEntityService(final DashboardFrameworkEntityDao dashboardFrameworkEntityDao) { + return new HibernateDashboardFrameworkEntityService(dashboardFrameworkEntityDao); + } + + @Bean + public ConstraintConfigDao constraintConfigDao(final SessionFactory sessionFactory) { + return new ConstraintConfigDao(sessionFactory); + } + + @Bean + public ConstraintConfigEntityService constraintConfigEntityService(final ConstraintConfigDao constraintConfigDao) { + HibernateConstraintConfigEntityService service = new HibernateConstraintConfigEntityService(); + service.setDao(constraintConfigDao); + return service; + } + + @Bean + public HardwareConfigDao hardwareConfigDao(final SessionFactory sessionFactory) { + return new HardwareConfigDao(sessionFactory); + } + + @Bean + public ComputeEnvironmentConfigDao computeEnvironmentConfigDao(final SessionFactory sessionFactory, + final HardwareConfigDao hardwareConfigDao) { + return new ComputeEnvironmentConfigDao(sessionFactory, hardwareConfigDao); + } + + @Bean + public ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService(final ComputeEnvironmentConfigDao computeEnvironmentConfigDao, + final HardwareConfigDao hardwareConfigDao) { + return new HibernateComputeEnvironmentConfigEntityService( + computeEnvironmentConfigDao, + hardwareConfigDao + ); + } + + @Bean + public HardwareConfigEntityService hardwareConfigEntityService(final HardwareConfigDao hardwareConfigDao) { + HibernateHardwareConfigEntityService service = new HibernateHardwareConfigEntityService(); + service.setDao(hardwareConfigDao); + return service; + } + + @Bean + public DashboardConfigDao dashboardConfigDao(final SessionFactory sessionFactory, + final ComputeEnvironmentConfigDao computeEnvironmentConfigDao, + final HardwareConfigDao hardwareConfigDao) { + return new DashboardConfigDao( + sessionFactory, + computeEnvironmentConfigDao, + hardwareConfigDao + ); + } + + @Bean + public DashboardConfigEntityService dashboardConfigEntityService(final DashboardConfigDao dashboardConfigDao) { + return new HibernateDashboardConfigEntityService( + dashboardConfigDao + ); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterAuthorizationConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterAuthorizationConfig.java deleted file mode 100644 index ebd3011..0000000 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterAuthorizationConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.nrg.xnatx.plugins.jupyterhub.config; - -import org.nrg.xdat.security.services.RoleHolder; -import org.nrg.xnatx.plugins.jupyterhub.authorization.JupyterUserAuthorization; -import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import({MockConfig.class}) -public class JupyterAuthorizationConfig { - - @Bean - public JupyterUserAuthorization defaultJupyterAuthorization(final RoleHolder mockRoleHolder, - final JupyterHubPreferences mockJupyterHubPreferences) { - return new JupyterUserAuthorization(mockRoleHolder, mockJupyterHubPreferences); - } - -} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubApiConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubApiConfig.java index a1f2ee5..69cb0db 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubApiConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubApiConfig.java @@ -4,6 +4,7 @@ import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xnat.tracking.services.EventTrackingDataHibernateService; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; import org.nrg.xnatx.plugins.jupyterhub.rest.JupyterHubApi; import org.nrg.xnatx.plugins.jupyterhub.services.JupyterHubService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; @@ -28,12 +29,14 @@ public JupyterHubApi jupyterHubApi(final UserManagementServiceI mockUserManageme final RoleHolder mockRoleHolder, final JupyterHubService mockJupyterHubService, final UserOptionsService mockUserOptionsService, - final EventTrackingDataHibernateService mockEventTrackingDataHibernateService) { + final EventTrackingDataHibernateService mockEventTrackingDataHibernateService, + final JupyterHubPreferences mockJupyterHubPreferences) { return new JupyterHubApi(mockUserManagementService, mockRoleHolder, mockJupyterHubService, mockUserOptionsService, - mockEventTrackingDataHibernateService); + mockEventTrackingDataHibernateService, + mockJupyterHubPreferences); } @Bean diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubDashboardConfigsApiTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubDashboardConfigsApiTestConfig.java new file mode 100644 index 0000000..a844ae3 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubDashboardConfigsApiTestConfig.java @@ -0,0 +1,47 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.framework.services.ContextService; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnatx.plugins.jupyterhub.rest.JupyterHubDashboardConfigsApi; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.TestingAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration +@EnableWebMvc +@EnableWebSecurity +@Import({MockConfig.class, RestApiTestConfig.class}) +public class JupyterHubDashboardConfigsApiTestConfig extends WebSecurityConfigurerAdapter { + + @Bean + public JupyterHubDashboardConfigsApi jupyterHubDashboardConfigsApi(final UserManagementServiceI mockUserManagementService, + final RoleHolder mockRoleHolder, + final DashboardConfigService mockDashboardConfigService) { + return new JupyterHubDashboardConfigsApi( + mockUserManagementService, + mockRoleHolder, + mockDashboardConfigService + ); + } + + @Bean + public ContextService contextService(final ApplicationContext applicationContext) { + final ContextService contextService = new ContextService(); + contextService.setApplicationContext(applicationContext); + return contextService; + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) { + auth.authenticationProvider(new TestingAuthenticationProvider()); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubDashboardFrameworksApiTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubDashboardFrameworksApiTestConfig.java new file mode 100644 index 0000000..9d0156a --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/JupyterHubDashboardFrameworksApiTestConfig.java @@ -0,0 +1,47 @@ +package org.nrg.xnatx.plugins.jupyterhub.config; + +import org.nrg.framework.services.ContextService; +import org.nrg.xdat.security.services.RoleHolder; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xnatx.plugins.jupyterhub.rest.JupyterHubDashboardFrameworksApi; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.TestingAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration +@EnableWebMvc +@EnableWebSecurity +@Import({MockConfig.class, RestApiTestConfig.class}) +public class JupyterHubDashboardFrameworksApiTestConfig extends WebSecurityConfigurerAdapter { + + @Bean + public JupyterHubDashboardFrameworksApi jupyterHubDashboardFrameworksApi(final UserManagementServiceI mockUserManagementService, + final RoleHolder mockRoleHolder, + final DashboardFrameworkService mockDashboardFrameworkService) { + return new JupyterHubDashboardFrameworksApi( + mockUserManagementService, + mockRoleHolder, + mockDashboardFrameworkService + ); + } + + @Bean + public ContextService contextService(final ApplicationContext applicationContext) { + final ContextService contextService = new ContextService(); + contextService.setApplicationContext(applicationContext); + return contextService; + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) { + auth.authenticationProvider(new TestingAuthenticationProvider()); + } + +} diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java index 898068b..cb27187 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/MockConfig.java @@ -6,6 +6,7 @@ import org.nrg.framework.services.SerializerService; import org.nrg.framework.utilities.OrderedProperties; import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; +import org.nrg.xnat.compute.services.ConstraintConfigService; import org.nrg.xnat.compute.services.HardwareConfigService; import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.prefs.services.NrgPreferenceService; @@ -161,6 +162,11 @@ public HardwareConfigService mockHardwareConfigService() { return Mockito.mock(HardwareConfigService.class); } + @Bean + public ConstraintConfigService mockConstraintConfigService() { + return Mockito.mock(ConstraintConfigService.class); + } + @Bean public JobTemplateService mockJobTemplateService() { return Mockito.mock(JobTemplateService.class); @@ -170,4 +176,19 @@ public JobTemplateService mockJobTemplateService() { public JupyterHubServiceAccountHelper mockJupyterHubServiceAccountHelper() { return Mockito.mock(JupyterHubServiceAccountHelper.class); } + + @Bean + public DashboardJobTemplateService mockDashboardJobTemplateService() { + return Mockito.mock(DashboardJobTemplateService.class); + } + + @Bean + public DashboardFrameworkService mockDashboardFrameworkService() { + return Mockito.mock(DashboardFrameworkService.class); + } + + @Bean + public DashboardConfigService mockDashboardConfigService() { + return Mockito.mock(DashboardConfigService.class); + } } diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/RestApiTestConfig.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/RestApiTestConfig.java index b0d862c..a80ee88 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/RestApiTestConfig.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/config/RestApiTestConfig.java @@ -28,6 +28,7 @@ @Configuration @Import({ObjectMapperConfig.class}) public class RestApiTestConfig extends WebMvcConfigurerAdapter { + @Bean @Qualifier("mockXnatAppInfo") public XnatAppInfo mockAppInfo() { diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/DashboardFrameworkInitializerTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/DashboardFrameworkInitializerTest.java new file mode 100644 index 0000000..cd4dea9 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/initialize/DashboardFrameworkInitializerTest.java @@ -0,0 +1,115 @@ +package org.nrg.xnatx.plugins.jupyterhub.initialize; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.xnat.initialization.tasks.InitializingTaskException; +import org.nrg.xnat.services.XnatAppInfo; +import org.nrg.xnatx.plugins.jupyterhub.config.DashboardFrameworkInitializerTestConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardFramework; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.nrg.xnatx.plugins.jupyterhub.utils.XFTManagerHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.isA; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = DashboardFrameworkInitializerTestConfig.class) +public class DashboardFrameworkInitializerTest { + + @Autowired private XFTManagerHelper mockXFTManagerHelper; + @Autowired private XnatAppInfo mockXnatAppInfo; + @Autowired private DashboardFrameworkService mockDashboardFrameworkService; + @Autowired private DashboardFrameworkInitializer initializer; + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + Mockito.reset( + mockXFTManagerHelper, + mockXnatAppInfo, + mockDashboardFrameworkService + ); + } + + @Test + public void test_wiring() { + assertNotNull(mockXFTManagerHelper); + assertNotNull(mockXnatAppInfo); + assertNotNull(mockDashboardFrameworkService); + } + + @Test + public void test_getTaskName() { + // Execute + final String taskName = initializer.getTaskName(); + + // Verify + assertThat(taskName, notNullValue()); + assertThat(taskName, isA(String.class)); + } + + @Test(expected = InitializingTaskException.class) + public void test_callImpl_XFTManagerHelperNotInitialized() throws InitializingTaskException { + // Set up + Mockito.when(mockXFTManagerHelper.isInitialized()).thenReturn(false); + + // Execute + initializer.callImpl(); + } + + @Test(expected = InitializingTaskException.class) + public void test_callImpl_XnatAppInfoNotInitialized() throws InitializingTaskException { + // Set up + Mockito.when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + Mockito.when(mockXnatAppInfo.isInitialized()).thenReturn(false); + + // Execute + initializer.callImpl(); + } + + @Test + public void test_callImpl() throws InitializingTaskException { + // Set up + Mockito.when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + Mockito.when(mockXnatAppInfo.isInitialized()).thenReturn(true); + + // Execute + initializer.callImpl(); + + // Verify + verify(mockDashboardFrameworkService, times(4)).create(any(DashboardFramework.class)); + } + + @Test + public void test_callImpl_dashboardFrameworkAlreadyExists() throws InitializingTaskException { + // Set up + Mockito.when(mockXFTManagerHelper.isInitialized()).thenReturn(true); + Mockito.when(mockXnatAppInfo.isInitialized()).thenReturn(true); + + Mockito.when(mockDashboardFrameworkService.get("Panel")).thenReturn(Optional.of(DashboardFramework.builder().name("Panel").build())); + Mockito.when(mockDashboardFrameworkService.get("Streamlit")).thenReturn(Optional.of(DashboardFramework.builder().name("Streamlit").build())); + Mockito.when(mockDashboardFrameworkService.get("Voila")).thenReturn(Optional.of(DashboardFramework.builder().name("Voila").build())); + Mockito.when(mockDashboardFrameworkService.get("Dash")).thenReturn(Optional.of(DashboardFramework.builder().name("Dash").build())); + + // Execute + initializer.callImpl(); + + // Verify + verify(mockDashboardFrameworkService, never()).create(any(DashboardFramework.class)); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java index 511d3b6..d2fc8fb 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubApiTest.java @@ -15,6 +15,7 @@ import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubApiConfig; import org.nrg.xnatx.plugins.jupyterhub.models.*; import org.nrg.xnatx.plugins.jupyterhub.models.docker.*; +import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; import org.nrg.xnatx.plugins.jupyterhub.services.JupyterHubService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.springframework.beans.factory.annotation.Autowired; @@ -35,6 +36,7 @@ import java.util.*; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; @@ -64,7 +66,6 @@ public class JupyterHubApiTest { private XnatUserOptions userOptions; private ServerStartRequest serverStartRequest; - private Long profileId; private Server dummyServer; private String servername; private Server dummyNamedServer; @@ -77,6 +78,7 @@ public class JupyterHubApiTest { @Autowired private RoleServiceI mockRoleService; @Autowired private UserManagementServiceI mockUserManagementService; @Autowired private UserOptionsService mockUserOptionsService; + @Autowired private JupyterHubPreferences mockJupyterHubPreferences; @Before public void before() throws Exception { @@ -139,7 +141,7 @@ public void before() throws Exception { .resources(resources) .build(); - profileId = 2L; + Long profileId = 2L; userOptions = XnatUserOptions.builder() .userId(nonAdminId) @@ -232,8 +234,20 @@ public void before() throws Exception { public void after() { Mockito.reset(admin); Mockito.reset(mockJupyterHubService); + Mockito.reset(mockRoleService); + Mockito.reset(mockUserManagementService); + Mockito.reset(mockUserOptionsService); + Mockito.reset(mockJupyterHubPreferences); } + @Test + public void testWiring() { + assertNotNull(mockJupyterHubService); + assertNotNull(mockRoleService); + assertNotNull(mockUserManagementService); + assertNotNull(mockUserOptionsService); + assertNotNull(mockJupyterHubPreferences); + } @Test public void testGetVersion() throws Exception { @@ -429,7 +443,93 @@ public void testGetServerDeniedWrongUser() throws Exception { } @Test - public void testStartServer() throws Exception { + public void testStartServer_AllUsers() throws Exception { + when(mockJupyterHubPreferences.getAllUsersCanStartJupyter()).thenReturn(true); + when(mockRoleService.checkRole(any(), eq("Jupyter"))).thenReturn(false); + when(mockRoleService.isSiteAdmin(nonAdmin)).thenReturn(false); + + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .post("/jupyterhub/users/" + NON_ADMIN_USERNAME + "/server") + .accept(JSON) + .contentType(JSON) + .content(mapper.writeValueAsString(serverStartRequest)) + .with(authentication(NONADMIN_AUTH)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isOk()); + + verify(mockJupyterHubService, times(1)).startServer(eq(nonAdmin), eq(serverStartRequest)); + } + + @Test + public void testStartServer_JupyterUsersRole() throws Exception { + when(mockJupyterHubPreferences.getAllUsersCanStartJupyter()).thenReturn(false); + when(mockRoleService.checkRole(any(), eq("Jupyter"))).thenReturn(true); + when(mockRoleService.isSiteAdmin(nonAdmin)).thenReturn(false); + + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .post("/jupyterhub/users/" + NON_ADMIN_USERNAME + "/server") + .accept(JSON) + .contentType(JSON) + .content(mapper.writeValueAsString(serverStartRequest)) + .with(authentication(NONADMIN_AUTH)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isOk()); + + verify(mockJupyterHubService, times(1)).startServer(eq(nonAdmin), eq(serverStartRequest)); + } + + @Test + public void testStartServer_Admin() throws Exception { + when(mockJupyterHubPreferences.getAllUsersCanStartJupyter()).thenReturn(false); + when(mockRoleService.checkRole(any(), eq("Jupyter"))).thenReturn(false); + when(mockRoleService.isSiteAdmin(admin)).thenReturn(true); + + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .post("/jupyterhub/users/" + ADMIN_USERNAME + "/server") + .accept(JSON) + .contentType(JSON) + .content(mapper.writeValueAsString(serverStartRequest)) + .with(authentication(ADMIN_AUTH)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + verify(mockJupyterHubService, never()).startServer(eq(admin), eq(serverStartRequest)); + } + + @Test + public void testStartServer_InsufficientPermissions() throws Exception { + when(mockJupyterHubPreferences.getAllUsersCanStartJupyter()).thenReturn(false); + when(mockRoleService.checkRole(any(), eq("Jupyter"))).thenReturn(false); + when(mockRoleService.isSiteAdmin(nonAdmin)).thenReturn(false); + + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .post("/jupyterhub/users/" + NON_ADMIN_USERNAME + "/server") + .accept(JSON) + .contentType(JSON) + .content(mapper.writeValueAsString(serverStartRequest)) + .with(authentication(NONADMIN_AUTH)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + verify(mockJupyterHubService, never()).startServer(eq(admin), eq(serverStartRequest)); + } + + @Test + public void testStartServer_Dashboard_nonJupyterUser() throws Exception { + when(mockJupyterHubPreferences.getAllUsersCanStartJupyter()).thenReturn(false); + when(mockRoleService.checkRole(any(), eq("Jupyter"))).thenReturn(false); + when(mockRoleService.isSiteAdmin(nonAdmin)).thenReturn(false); + + serverStartRequest.setDashboardConfigId(1L); // All users can start dashboard servers + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders .post("/jupyterhub/users/" + NON_ADMIN_USERNAME + "/server") .accept(JSON) diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardConfigsApiTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardConfigsApiTest.java new file mode 100644 index 0000000..02d494f --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardConfigsApiTest.java @@ -0,0 +1,437 @@ +package org.nrg.xnatx.plugins.jupyterhub.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.framework.constants.Scope; +import org.nrg.xapi.exceptions.NotFoundException; +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xft.security.UserI; +import org.nrg.xnat.compute.models.ComputeEnvironmentConfig; +import org.nrg.xnat.compute.models.HardwareConfig; +import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubDashboardConfigsApiTestConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.Dashboard; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardScope; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.util.*; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.nrg.framework.constants.Scope.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebAppConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {JupyterHubDashboardConfigsApiTestConfig.class}) +public class JupyterHubDashboardConfigsApiTest { + + @Autowired private WebApplicationContext wac; + @Autowired private ObjectMapper mapper; + @Autowired private RoleServiceI mockRoleService; + @Autowired private UserManagementServiceI mockUserManagementService; + @Autowired private DashboardConfigService mockDashboardConfigService; + @Autowired private JupyterHubDashboardConfigsApi jupyterHubDashboardConfigsApi; + + private MockMvc mockMvc; + private UserI mockUser; + private Authentication mockAuthentication; + + private DashboardConfig dashboardConfig1; + private DashboardConfig dashboardConfig2; + + @Before + public void setup() { + // Setup mocks + mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); + + mockUser = Mockito.mock(UserI.class); + when(mockUser.getLogin()).thenReturn("mockUser"); + when(mockUser.getEmail()).thenReturn("mockUser@mockuser.com"); + when(mockUser.getPassword()).thenReturn("mockUserPassword"); + when(mockUser.getID()).thenReturn(1); + when(mockRoleService.isSiteAdmin(mockUser)).thenReturn(false); + mockAuthentication = new TestingAuthenticationToken(mockUser, mockUser.getPassword()); + + // Setup dashboard config + Dashboard dashboard1 = Dashboard.builder() + .name("Panel Dashboard") + .description("A dashboard using Panel") + .framework("Panel") + .command(null) + .fileSource("git") + .gitRepoUrl("https://github.com/andylassiter/dashboard-testing.git") + .gitRepoBranch("main") + .mainFilePath("pane/panel_dashboard.ipynb") + .build(); + + + + DashboardScope dashboardSiteScope1 = DashboardScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + DashboardScope dashboardProjectScope1 = DashboardScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(new ArrayList<>(Arrays.asList("Project1", "Project2", "Project3")))) + .build(); + + DashboardScope dashboardDatatypeScope1 = DashboardScope.builder() + .scope(DataType) + .enabled(false) + .ids(new HashSet<>(Arrays.asList("xnat:mrSessionData", "xnat:petSessionData", "xnat:ctSessionData", "xnat:projectData"))) + .build(); + + + Map dashboardScopes1 = new HashMap<>(); + dashboardScopes1.put(Site, dashboardSiteScope1); + dashboardScopes1.put(Project, dashboardProjectScope1); + dashboardScopes1.put(DataType, dashboardDatatypeScope1); + + dashboardConfig1 = DashboardConfig.builder() + .dashboard(dashboard1) + .scopes(dashboardScopes1) + .computeEnvironmentConfig(ComputeEnvironmentConfig.builder().id(1L).build()) + .hardwareConfig(HardwareConfig.builder().id(1L).build()) + .build(); + + // Setup second dashboard config + Dashboard dashboard2 = Dashboard.builder() + .name("Voila Dashboard") + .description("A dashboard using Voila") + .framework("Voila") + .command(null) + .fileSource("git") + .gitRepoUrl("https://github.com/andylassiter/dashboard-testing.git") + .gitRepoBranch("main") + .mainFilePath("voila/voila_dashboard.ipynb") + .build(); + + DashboardScope dashboardSiteScope2 = DashboardScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + DashboardScope dashboardProjectScope2 = DashboardScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(new ArrayList<>(Collections.singletonList("Project1")))) + .build(); + + DashboardScope dashboardDatatypeScope2 = DashboardScope.builder() + .scope(DataType) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("xnat:projectData"))) + .build(); + + Map dashboardScopes2 = new HashMap<>(); + dashboardScopes2.put(Site, dashboardSiteScope2); + dashboardScopes2.put(Project, dashboardProjectScope2); + dashboardScopes2.put(DataType, dashboardDatatypeScope2); + + dashboardConfig2 = DashboardConfig.builder() + .dashboard(dashboard2) + .scopes(dashboardScopes2) + .computeEnvironmentConfig(ComputeEnvironmentConfig.builder().id(2L).build()) + .hardwareConfig(HardwareConfig.builder().id(2L).build()) + .build(); + } + + @After + public void tearDown() { + Mockito.reset( + mockRoleService, + mockUserManagementService, + mockDashboardConfigService, + mockUser + ); + } + + @Test + public void test_wiring() { + assertNotNull(wac); + assertNotNull(mapper); + assertNotNull(mockRoleService); + assertNotNull(mockUserManagementService); + assertNotNull(mockDashboardConfigService); + assertNotNull(jupyterHubDashboardConfigsApi); + } + + @Test + public void test_getDashboardConfigs() throws Exception { + // Setup mocks + when(mockDashboardConfigService.getAll()).thenReturn(Arrays.asList(dashboardConfig1, dashboardConfig2)); + + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.get("/jupyterhub/dashboards/configs") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + + final String response = mockMvc.perform(request) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + assertNotNull(response); + final List dashboardConfigs = Arrays.asList(mapper.readValue(response, DashboardConfig[].class)); + assertThat(dashboardConfigs.size(), is(2)); + assertThat(dashboardConfigs, hasItems(dashboardConfig1, dashboardConfig2)); + verify(mockDashboardConfigService).getAll(); + } + + @Test + public void test_getDashboardConfig() throws Exception { + // Setup mocks + when(mockDashboardConfigService.retrieve(1L)).thenReturn(Optional.of(dashboardConfig1)); + + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.get("/jupyterhub/dashboards/configs/1") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + final String response = mockMvc.perform(request) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + assertNotNull(response); + final DashboardConfig dashboardConfig = mapper.readValue(response, DashboardConfig.class); + assertThat(dashboardConfig, is(dashboardConfig1)); + verify(mockDashboardConfigService).retrieve(1L); + } + + @Test + public void test_getDashboardConfig_notFound() throws Exception { + // Setup mocks + when(mockDashboardConfigService.retrieve(1L)).thenReturn(Optional.empty()); + + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.get("/jupyterhub/dashboards/configs/1") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request) + .andExpect(status().isNotFound()) // 404 Not Found + .andReturn() + .getResponse() + .getContentAsString(); + + verify(mockDashboardConfigService).retrieve(1L); + } + + @Test + public void test_createDashboardConfig() throws Exception { + // Setup mocks + when(mockDashboardConfigService.create(dashboardConfig1)).thenReturn(dashboardConfig1); + + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.post("/jupyterhub/dashboards/configs") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()) + .content(mapper.writeValueAsString(dashboardConfig1)) + .contentType("application/json"); + + final String response = mockMvc.perform(request) + .andExpect(status().isCreated()) // 201 Created + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + assertNotNull(response); + final DashboardConfig dashboardConfig = mapper.readValue(response, DashboardConfig.class); + assertThat(dashboardConfig, is(dashboardConfig1)); + + verify(mockDashboardConfigService).create(dashboardConfig1); + } + + @Test + public void test_updateDashboardConfig() throws Exception { + // Setup mocks + dashboardConfig1.setId(1L); + when(mockDashboardConfigService.update(dashboardConfig1)).thenReturn(dashboardConfig1); + + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.put("/jupyterhub/dashboards/configs/1") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()) + .content(mapper.writeValueAsString(dashboardConfig1)) + .contentType("application/json"); + + final String response = mockMvc.perform(request) + .andExpect(status().isOk()) // 200 OK + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + assertNotNull(response); + final DashboardConfig dashboardConfig = mapper.readValue(response, DashboardConfig.class); + assertThat(dashboardConfig, is(dashboardConfig1)); + verify(mockDashboardConfigService).update(dashboardConfig1); + } + + @Test + public void test_updateDashboardConfig_notFound() throws Exception { + // Setup mocks + dashboardConfig1.setId(1L); + when(mockDashboardConfigService.update(dashboardConfig1)).thenThrow(new NotFoundException("Dashboard config not found.")); + + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.put("/jupyterhub/dashboards/configs/1") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()) + .content(mapper.writeValueAsString(dashboardConfig1)) + .contentType("application/json"); + + mockMvc.perform(request) + .andExpect(status().isNotFound()) // 404 Not Found + .andReturn() + .getResponse() + .getContentAsString(); + } + + @Test + public void test_deleteDashboardConfig() throws Exception { + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.delete("/jupyterhub/dashboards/configs/1") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request) + .andExpect(status().isNoContent()) // 204 No Content + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + verify(mockDashboardConfigService).delete(1L); + } + + @Test + public void test_getAvailableDashboardConfigs() throws Exception { + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get("/jupyterhub/dashboards/configs/available") + .param("site", "XNAT") + .param("user", mockUser.getLogin()) + .param("prj", "projectId") + .param("datatype", "xnat:mrSessionData") + .param("type", "JUPYTERHUB") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isOk()); + + // Verify conversion of parameters to scope map + Map expectedScopeMap = new HashMap<>(); + expectedScopeMap.put(Site, "XNAT"); + expectedScopeMap.put(User, mockUser.getLogin()); + expectedScopeMap.put(Project, "projectId"); + expectedScopeMap.put(DataType, "xnat:mrSessionData"); + + verify(mockDashboardConfigService).getAvailable(eq(expectedScopeMap)); + } + + @Test + public void test_enableDashboardConfigAtSite() throws Exception { + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .post("/jupyterhub/dashboards/configs/1/scope/site") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isNoContent()); + + // Verify + verify(mockDashboardConfigService).enableForSite(1L); + } + + @Test + public void test_disableDashboardConfigAtSite() throws Exception { + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .delete("/jupyterhub/dashboards/configs/1/scope/site") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isNoContent()); + + // Verify + verify(mockDashboardConfigService).disableForSite(1L); + } + + @Test + public void test_enableDashboardConfigAtProject() throws Exception { + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .post("/jupyterhub/dashboards/configs/1/scope/project/projectId") + .param("projectId", "projectId") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isNoContent()); + + // Verify + verify(mockDashboardConfigService).enableForProject(1L, "projectId"); + } + + @Test + public void test_disableDashboardConfigAtProject() throws Exception { + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .delete("/jupyterhub/dashboards/configs/1/scope/project/projectId") + .param("projectId", "projectId") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request).andExpect(status().isNoContent()); + + // Verify + verify(mockDashboardConfigService).disableForProject(1L, "projectId"); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardFrameworksApiTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardFrameworksApiTest.java new file mode 100644 index 0000000..2e1dda0 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/rest/JupyterHubDashboardFrameworksApiTest.java @@ -0,0 +1,214 @@ +package org.nrg.xnatx.plugins.jupyterhub.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.xdat.security.services.RoleServiceI; +import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xft.security.UserI; +import org.nrg.xnatx.plugins.jupyterhub.config.JupyterHubDashboardFrameworksApiTestConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardFramework; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebAppConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {JupyterHubDashboardFrameworksApiTestConfig.class}) +public class JupyterHubDashboardFrameworksApiTest { + + @Autowired private WebApplicationContext wac; + @Autowired private ObjectMapper mapper; + @Autowired private RoleServiceI mockRoleService; + @Autowired private UserManagementServiceI mockUserManagementService; + @Autowired private DashboardFrameworkService mockDashboardFrameworkService; + + private MockMvc mockMvc; + private UserI mockUser; + private Authentication mockAuthentication; + + private DashboardFramework panel; + private DashboardFramework streamlit; + private List frameworks; + + @Before + public void setup() { + // Setup dashboards + panel = DashboardFramework.builder() + .name("Panel") + .commandTemplate("jhsingle-native-proxy --destport {port} --authtype none --user {username} --group {group} --debug") + .build(); + streamlit = DashboardFramework.builder() + .name("Streamlit") + .commandTemplate("jhsingle-native-proxy --destport {port} --authtype none --user {username} --group {group} --debug") + .build(); + + frameworks = Arrays.asList(panel, streamlit); + + // Setup mocks + mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); + + mockUser = Mockito.mock(UserI.class); + when(mockUser.getLogin()).thenReturn("mockUser"); + when(mockUser.getEmail()).thenReturn("mockUser@mockuser.com"); + when(mockUser.getPassword()).thenReturn("mockUserPassword"); + when(mockUser.getID()).thenReturn(1); + when(mockRoleService.isSiteAdmin(mockUser)).thenReturn(false); + mockAuthentication = new TestingAuthenticationToken(mockUser, mockUser.getPassword()); + } + + @After + public void after() throws Exception { + Mockito.reset( + mockRoleService, + mockUserManagementService, + mockDashboardFrameworkService, + mockUser + ); + } + + @Test + public void test_wiring() { + assertNotNull(wac); + assertNotNull(mapper); + assertNotNull(mockRoleService); + assertNotNull(mockUserManagementService); + assertNotNull(mockDashboardFrameworkService); + } + + @Test + public void test_getAll() throws Exception { + // Setup + when(mockDashboardFrameworkService.getAll()).thenReturn(frameworks); + + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.get("/jupyterhub/dashboards/frameworks") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + final String response = mockMvc.perform(request) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + assertNotNull(response); + assertEquals(mapper.writeValueAsString(frameworks), response); + verify(mockDashboardFrameworkService).getAll(); + } + + @Test + public void test_get() throws Exception { + // Setup + when(mockDashboardFrameworkService.get(panel.getName())).thenReturn(java.util.Optional.of(panel)); + + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.get("/jupyterhub/dashboards/frameworks/" + panel.getName()) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + final String response = mockMvc.perform(request) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + assertNotNull(response); + assertEquals(mapper.writeValueAsString(panel), response); + verify(mockDashboardFrameworkService).get(panel.getName()); + } + + @Test + public void test_create() throws Exception { + // Setup + when(mockDashboardFrameworkService.create(panel)).thenReturn(panel); + + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.post("/jupyterhub/dashboards/frameworks") + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()) + .content(mapper.writeValueAsString(panel)) + .contentType("application/json"); + + final String response = mockMvc.perform(request) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + assertNotNull(response); + assertEquals(mapper.writeValueAsString(panel), response); + verify(mockDashboardFrameworkService).create(panel); + } + + @Test + public void test_update() throws Exception { + // Setup + when(mockDashboardFrameworkService.update(panel)).thenReturn(panel); + + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.put("/jupyterhub/dashboards/frameworks/" + panel.getName()) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()) + .content(mapper.writeValueAsString(panel)) + .contentType("application/json"); + + final String response = mockMvc.perform(request) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + assertNotNull(response); + assertEquals(mapper.writeValueAsString(panel), response); + verify(mockDashboardFrameworkService).update(panel); + } + + @Test + public void test_delete() throws Exception { + // Setup + // Execute + final MockHttpServletRequestBuilder request = MockMvcRequestBuilders.delete("/jupyterhub/dashboards/frameworks/" + panel.getName()) + .with(authentication(mockAuthentication)) + .with(csrf()) + .with(testSecurityContext()); + + mockMvc.perform(request) + .andExpect(status().isOk()); + + // Verify + verify(mockDashboardFrameworkService).delete(panel.getName()); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardConfigServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardConfigServiceTest.java new file mode 100644 index 0000000..6ea078a --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardConfigServiceTest.java @@ -0,0 +1,1442 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.nrg.framework.constants.Scope; +import org.nrg.xnat.compute.models.*; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigEntityService; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; +import org.nrg.xnat.compute.services.HardwareConfigEntityService; +import org.nrg.xnat.compute.services.HardwareConfigService; +import org.nrg.xnatx.plugins.jupyterhub.config.DefaultDashboardConfigServiceTestConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.Dashboard; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardFramework; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardScope; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkEntityService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.transaction.Transactional; +import java.util.*; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.core.IsNot.not; +import static org.junit.Assert.*; +import static org.nrg.framework.constants.Scope.*; +import static org.nrg.xnat.compute.models.ComputeEnvironmentConfig.ConfigType.CONTAINER_SERVICE; +import static org.nrg.xnat.compute.models.ComputeEnvironmentConfig.ConfigType.JUPYTERHUB; +import static org.nrg.xnatx.plugins.jupyterhub.utils.TestingUtils.commitTransaction; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes={DefaultDashboardConfigServiceTestConfig.class}) +@Transactional +public class DefaultDashboardConfigServiceTest { + + @Autowired private DefaultDashboardConfigService defaultDashboardConfigService; + @Autowired private DashboardConfigEntityService dashboardConfigEntityService; + @Autowired private ComputeEnvironmentConfigEntityService computeEnvironmentConfigEntityService; + @Autowired private ComputeEnvironmentConfigService computeEnvironmentConfigService; + @Autowired private HardwareConfigEntityService hardwareConfigEntityService; + @Autowired private HardwareConfigService hardwareConfigService; + @Autowired private DashboardFrameworkService dashboardFrameworkService; + + private ComputeEnvironmentConfig computeEnvironmentConfig1; + private ComputeEnvironmentConfig computeEnvironmentConfig2; + private ComputeEnvironmentConfig computeEnvironmentConfig3; + + private HardwareConfig hardwareConfig1; + private HardwareConfig hardwareConfig2; + private HardwareConfig hardwareConfig3; + + private DashboardFramework panel; + private DashboardFramework streamlit; + private DashboardFramework voila; + private DashboardFramework dash; + + private DashboardConfig dashboardConfig1; + private DashboardConfig dashboardConfig2; + private DashboardConfig dashboardConfig3; + private DashboardConfig dashboardConfigInvalid; + + @Before + public void setUp() { + createDummyConfigs(); + } + + @After + public void tearDown() { + } + + @Test + public void test_wiring() { + assertNotNull(defaultDashboardConfigService); + assertNotNull(dashboardConfigEntityService); + assertNotNull(computeEnvironmentConfigEntityService); + assertNotNull(computeEnvironmentConfigService); + assertNotNull(hardwareConfigEntityService); + assertNotNull(hardwareConfigService); + } + + @Test + public void testValidateDashboardConfigs() { + defaultDashboardConfigService.validate(dashboardConfig1); + defaultDashboardConfigService.validate(dashboardConfig2); + defaultDashboardConfigService.validate(dashboardConfig3); + + assertTrue(defaultDashboardConfigService.isValid(dashboardConfig1)); + assertTrue(defaultDashboardConfigService.isValid(dashboardConfig2)); + assertTrue(defaultDashboardConfigService.isValid(dashboardConfig3)); + } + + @Test + public void testValidateDashboards() { + defaultDashboardConfigService.validate(dashboardConfig1.getDashboard()); + defaultDashboardConfigService.validate(dashboardConfig2.getDashboard()); + defaultDashboardConfigService.validate(dashboardConfig3.getDashboard()); + } + + @Test + public void testValidateDashboardScopes() { + defaultDashboardConfigService.validate(dashboardConfig1.getScopes()); + defaultDashboardConfigService.validate(dashboardConfig2.getScopes()); + defaultDashboardConfigService.validate(dashboardConfig3.getScopes()); + } + + @Test(expected = IllegalArgumentException.class) + public void test_InvalidDashboardConfig_NoComputeEnvironmentConfig() { + dashboardConfigInvalid = dashboardConfig1; + dashboardConfigInvalid.setComputeEnvironmentConfig(null); + defaultDashboardConfigService.validate(dashboardConfigInvalid); + } + + @Test + public void test_InvalidDashboardConfig_NoComputeEnvironmentConfig_2() { + dashboardConfigInvalid = dashboardConfig1; + dashboardConfigInvalid.setComputeEnvironmentConfig(null); + assertFalse(defaultDashboardConfigService.isValid(dashboardConfigInvalid)); + } + + @Test(expected = IllegalArgumentException.class) + public void test_InvalidDashboardConfig_NoHardwareConfig() { + dashboardConfigInvalid = dashboardConfig1; + dashboardConfigInvalid.setHardwareConfig(null); + defaultDashboardConfigService.validate(dashboardConfigInvalid); + } + + @Test(expected = IllegalArgumentException.class) + public void test_InvalidDashboardConfig_NoDashboard() { + dashboardConfigInvalid = dashboardConfig1; + dashboardConfigInvalid.setDashboard(null); + defaultDashboardConfigService.validate(dashboardConfigInvalid); + } + + @Test(expected = IllegalArgumentException.class) + public void test_InvalidDashboardConfig_NoScopes() { + dashboardConfigInvalid = dashboardConfig1; + dashboardConfigInvalid.setScopes(null); + defaultDashboardConfigService.validate(dashboardConfigInvalid); + } + + @Test(expected = IllegalArgumentException.class) + public void test_InvalidDashboardConfig_EmptyScopes() { + dashboardConfigInvalid = dashboardConfig1; + dashboardConfigInvalid.setScopes(new HashMap<>()); + defaultDashboardConfigService.validate(dashboardConfigInvalid); + } + + @Test(expected = IllegalArgumentException.class) + public void test_InvalidDashboardConfig_NoScopeSite() { + dashboardConfigInvalid = dashboardConfig1; + dashboardConfigInvalid.getScopes().remove(Site); + defaultDashboardConfigService.validate(dashboardConfigInvalid); + } + + @Test(expected = IllegalArgumentException.class) + public void test_InvalidDashboardConfig_NoScopeProject() { + dashboardConfigInvalid = dashboardConfig1; + dashboardConfigInvalid.getScopes().remove(Project); + defaultDashboardConfigService.validate(dashboardConfigInvalid); + } + + @Test(expected = IllegalArgumentException.class) + public void test_InvalidDashboardConfig_NoScopeDataType() { + dashboardConfigInvalid = dashboardConfig1; + dashboardConfigInvalid.getScopes().remove(DataType); + defaultDashboardConfigService.validate(dashboardConfigInvalid); + } + + @Test + @DirtiesContext + public void test_create() { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard config + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Verify created dashboard configs + assertNotNull(created1); + assertNotNull(created2); + assertNotNull(created3); + + assertEquals(dashboardConfig1.getDashboard(), created1.getDashboard()); + assertEquals(dashboardConfig2.getDashboard(), created2.getDashboard()); + assertEquals(dashboardConfig3.getDashboard(), created3.getDashboard()); + + assertEquals(dashboardConfig1.getScopes(), created1.getScopes()); + assertEquals(dashboardConfig2.getScopes(), created2.getScopes()); + assertEquals(dashboardConfig3.getScopes(), created3.getScopes()); + + // When checking pojos, the compute environment and hardware configs will only have ids and names, not the full objects + assertEquals(dashboardConfig1.getComputeEnvironmentConfig().getId(), created1.getComputeEnvironmentConfig().getId()); + assertEquals(dashboardConfig2.getComputeEnvironmentConfig().getId(), created2.getComputeEnvironmentConfig().getId()); + assertEquals(dashboardConfig3.getComputeEnvironmentConfig().getId(), created3.getComputeEnvironmentConfig().getId()); + + assertEquals(dashboardConfig1.getHardwareConfig().getId(), created1.getHardwareConfig().getId()); + assertEquals(dashboardConfig2.getHardwareConfig().getId(), created2.getHardwareConfig().getId()); + assertEquals(dashboardConfig3.getHardwareConfig().getId(), created3.getHardwareConfig().getId()); + } + + @Test(expected = IllegalArgumentException.class) + @DirtiesContext + public void test_create_invalid() { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Create dashboard config + DashboardConfig createdInvalid = defaultDashboardConfigService.create(dashboardConfigInvalid); + + commitTransaction(); + } + + @Test + @DirtiesContext + public void test_exists() { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard config + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Verify created dashboard configs + assertNotNull(created1); + assertNotNull(created2); + assertNotNull(created3); + + assertTrue(defaultDashboardConfigService.exists(created1.getId())); + assertTrue(defaultDashboardConfigService.exists(created2.getId())); + assertTrue(defaultDashboardConfigService.exists(created3.getId())); + } + + @Test + @DirtiesContext + public void test_retrieve() { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard config + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Verify created dashboard configs + assertNotNull(created1); + assertNotNull(created2); + assertNotNull(created3); + + Optional retrieved1 = defaultDashboardConfigService.retrieve(created1.getId()); + Optional retrieved2 = defaultDashboardConfigService.retrieve(created2.getId()); + Optional retrieved3 = defaultDashboardConfigService.retrieve(created3.getId()); + + assertTrue(retrieved1.isPresent()); + assertTrue(retrieved2.isPresent()); + assertTrue(retrieved3.isPresent()); + + assertEquals(created1, retrieved1.get()); + assertEquals(created2, retrieved2.get()); + assertEquals(created3, retrieved3.get()); + } + + @Test + @DirtiesContext + public void test_retrieve_dne() { + Optional retrievedDne = defaultDashboardConfigService.retrieve(999L); + assertFalse(retrievedDne.isPresent()); + } + + @Test + @DirtiesContext + public void test_getAll() { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard config + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Verify created dashboard configs + assertNotNull(created1); + assertNotNull(created2); + assertNotNull(created3); + + List retrieved = defaultDashboardConfigService.getAll(); + + assertThat(retrieved.size(), is(3)); + assertThat(retrieved, hasItems(created1, created2, created3)); + } + + @Test + @DirtiesContext + public void test_update_updateDashboard() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard config + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Update dashboard config + dashboardConfig1.setId(created1.getId()); + dashboardConfig1.getDashboard().setName("New Name"); + dashboardConfig1.getDashboard().setDescription("New Description"); + dashboardConfig1.getDashboard().setFramework(streamlit.getName()); + dashboardConfig1.getDashboard().setCommand("New Command"); + dashboardConfig1.getDashboard().setFileSource("New File Source"); + dashboardConfig1.getDashboard().setGitRepoUrl("New Git Repo Url"); + dashboardConfig1.getDashboard().setGitRepoBranch("New Git Repo Branch"); + dashboardConfig1.getDashboard().setMainFilePath("New Main File Path"); + + DashboardConfig updated1 = defaultDashboardConfigService.update(dashboardConfig1); + + commitTransaction(); + + // Verify updated dashboard config + assertNotNull(updated1); + + // These are not equal because the updated dashboardConfig# has the full compute environment and hardware config + // pojos while the retrieved updated1 only has the ids and names + // assertEquals(dashboardConfig1, updated1); + + assertEquals(dashboardConfig1.getDashboard(), updated1.getDashboard()); + assertNotEquals(created1.getDashboard(), updated1.getDashboard()); + assertEquals(dashboardConfig1.getScopes(), updated1.getScopes()); + assertEquals(dashboardConfig1.getComputeEnvironmentConfig().getId(), + updated1.getComputeEnvironmentConfig().getId()); + assertEquals(dashboardConfig1.getComputeEnvironmentConfig().getComputeEnvironment().getName(), + updated1.getComputeEnvironmentConfig().getComputeEnvironment().getName()); + assertEquals(dashboardConfig1.getHardwareConfig().getId(), + updated1.getHardwareConfig().getId()); + assertEquals(dashboardConfig1.getHardwareConfig().getHardware().getName(), + updated1.getHardwareConfig().getHardware().getName()); + } + + @Test + @DirtiesContext + public void test_update_updateScopes() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard config + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Update dashboard config + dashboardConfig1.setId(created1.getId()); + dashboardConfig1.getScopes().get(Site).setEnabled(false); + dashboardConfig1.getScopes().get(Project).setIds(new HashSet<>(Arrays.asList("ProjectA", "ProjectB", "ProjectC"))); + dashboardConfig1.getScopes().get(DataType).setIds(new HashSet<>(Arrays.asList("pixi:bliSessionData", "xnat:petSessionData"))); + + DashboardConfig updated1 = defaultDashboardConfigService.update(dashboardConfig1); + + commitTransaction(); + + // Verify updated dashboard config + assertNotNull(updated1); + + // These are not equal because the updated dashboardConfig# has the full compute environment and hardware config + // pojos while the retrieved updated1 only has the ids and names + // assertEquals(dashboardConfig1, updated1); + + assertEquals(dashboardConfig1.getDashboard(), updated1.getDashboard()); + assertEquals(dashboardConfig1.getScopes(), updated1.getScopes()); + assertNotEquals(created1.getScopes(), updated1.getScopes()); + assertEquals(dashboardConfig1.getComputeEnvironmentConfig().getId(), + updated1.getComputeEnvironmentConfig().getId()); + assertEquals(dashboardConfig1.getComputeEnvironmentConfig().getComputeEnvironment().getName(), + updated1.getComputeEnvironmentConfig().getComputeEnvironment().getName()); + assertEquals(dashboardConfig1.getHardwareConfig().getId(), + updated1.getHardwareConfig().getId()); + assertEquals(dashboardConfig1.getHardwareConfig().getHardware().getName(), + updated1.getHardwareConfig().getHardware().getName()); + } + + @Test + @DirtiesContext + public void test_update_updateComputeEnvAndHardware() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard config + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Update dashboard config + dashboardConfig1.setId(created1.getId()); + dashboardConfig1.setComputeEnvironmentConfig(computeEnvironmentConfig2); + dashboardConfig1.setHardwareConfig(hardwareConfig2); + + DashboardConfig updated1 = defaultDashboardConfigService.update(dashboardConfig1); + + commitTransaction(); + + // Verify updated dashboard config + assertNotNull(updated1); + + // These are not equal because the updated dashboardConfig# has the full compute environment and hardware config + // pojos while the retrieved updated1 only has the ids and names + // assertEquals(dashboardConfig1, updated1); + + assertEquals(dashboardConfig1.getDashboard(), updated1.getDashboard()); + assertEquals(dashboardConfig1.getScopes(), updated1.getScopes()); + assertEquals(dashboardConfig1.getComputeEnvironmentConfig().getId(), + updated1.getComputeEnvironmentConfig().getId()); + assertEquals(dashboardConfig1.getComputeEnvironmentConfig().getComputeEnvironment().getName(), + updated1.getComputeEnvironmentConfig().getComputeEnvironment().getName()); + assertNotEquals(created1.getComputeEnvironmentConfig().getId(), + updated1.getComputeEnvironmentConfig().getId()); + assertNotEquals(created1.getComputeEnvironmentConfig().getComputeEnvironment().getName(), + updated1.getComputeEnvironmentConfig().getComputeEnvironment().getName()); + assertEquals(dashboardConfig1.getHardwareConfig().getId(), + updated1.getHardwareConfig().getId()); + assertEquals(dashboardConfig1.getHardwareConfig().getHardware().getName(), + updated1.getHardwareConfig().getHardware().getName()); + assertNotEquals(created1.getHardwareConfig().getId(), + updated1.getHardwareConfig().getId()); + assertNotEquals(created1.getHardwareConfig().getHardware().getName(), + updated1.getHardwareConfig().getHardware().getName()); + } + + @Test + @DirtiesContext + public void testDelete() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard config + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Verify created dashboard configs + assertNotNull(created1); + assertNotNull(created2); + assertNotNull(created3); + + // Delete dashboard config + defaultDashboardConfigService.delete(created1.getId()); + defaultDashboardConfigService.delete(created2.getId()); + defaultDashboardConfigService.delete(created3.getId()); + + commitTransaction(); + + // Verify deleted dashboard configs + assertFalse(defaultDashboardConfigService.exists(created1.getId())); + assertFalse(defaultDashboardConfigService.exists(created2.getId())); + assertFalse(defaultDashboardConfigService.exists(created3.getId())); + } + + @Test + @DirtiesContext + public void test_isAvailable() { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard config + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Verify created dashboard configs + assertNotNull(created1); + assertNotNull(created2); + assertNotNull(created3); + + Map executionScope = new HashMap<>(); + executionScope.put(Scope.Site, "XNAT"); + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:mrSessionData"); + + assertTrue(defaultDashboardConfigService.isAvailable(created1.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project2"); + assertTrue(defaultDashboardConfigService.isAvailable(created1.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project3"); + assertTrue(defaultDashboardConfigService.isAvailable(created1.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project4"); + assertFalse(defaultDashboardConfigService.isAvailable(created1.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:petSessionData"); + assertTrue(defaultDashboardConfigService.isAvailable(created1.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "pixi:bliSessionData"); + assertFalse(defaultDashboardConfigService.isAvailable(created1.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:mrSessionData"); + executionScope.put(Scope.User, "User1"); // Should not matter + assertTrue(defaultDashboardConfigService.isAvailable(created1.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:ctSessionData"); + assertTrue(defaultDashboardConfigService.isAvailable(created1.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project2"); + executionScope.put(Scope.DataType, "xnat:ctSessionData"); + assertFalse(defaultDashboardConfigService.isAvailable(created2.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:projectData"); + assertTrue(defaultDashboardConfigService.isAvailable(created2.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:ctSessionData"); + assertFalse(defaultDashboardConfigService.isAvailable(created2.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:projectData"); + assertFalse(defaultDashboardConfigService.isAvailable(created3.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project2"); + executionScope.put(Scope.DataType, "xnat:projectData"); + assertFalse(defaultDashboardConfigService.isAvailable(created3.getId(), executionScope)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:mrSessionData"); + assertFalse(defaultDashboardConfigService.isAvailable(created3.getId(), executionScope)); + } + + @Test + @DirtiesContext + public void test_getAvailable() { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard configs + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Verify created dashboard configs + assertNotNull(created1); + assertNotNull(created2); + assertNotNull(created3); + + Map executionScope = new HashMap<>(); + executionScope.put(Scope.Site, "XNAT"); + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:mrSessionData"); + + List available = defaultDashboardConfigService.getAvailable(executionScope); + + assertThat(available.size(), is(1)); + assertThat(available, hasItems(created1)); + + executionScope.put(Scope.Project, "Project2"); + available = defaultDashboardConfigService.getAvailable(executionScope); + + assertThat(available.size(), is(1)); + assertThat(available, hasItems(created1)); + + executionScope.put(Scope.Project, "Project3"); + available = defaultDashboardConfigService.getAvailable(executionScope); + + assertThat(available.size(), is(1)); + assertThat(available, hasItems(created1)); + + executionScope.put(Scope.Project, "Project4"); + available = defaultDashboardConfigService.getAvailable(executionScope); + + assertThat(available.size(), is(0)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:petSessionData"); + available = defaultDashboardConfigService.getAvailable(executionScope); + + assertThat(available.size(), is(1)); + assertThat(available, hasItems(created1)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "pixi:bliSessionData"); + available = defaultDashboardConfigService.getAvailable(executionScope); + + assertThat(available.size(), is(0)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:mrSessionData"); + executionScope.put(Scope.User, "User1"); // Should not matter + available = defaultDashboardConfigService.getAvailable(executionScope); + + assertThat(available.size(), is(1)); + assertThat(available, hasItems(created1)); + + executionScope.put(Scope.Project, "Project1"); + executionScope.put(Scope.DataType, "xnat:projectData"); + available = defaultDashboardConfigService.getAvailable(executionScope); + + assertThat(available.size(), is(2)); + assertThat(available, hasItems(created1, created2)); + + executionScope.put(Scope.Project, "Project2"); + executionScope.put(Scope.DataType, "xnat:projectData"); + available = defaultDashboardConfigService.getAvailable(executionScope); + + assertThat(available.size(), is(1)); + assertThat(available, hasItems(created1)); + } + + @Test + @DirtiesContext + public void test_enableForSite() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard configs + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Verify created dashboard configs + assertNotNull(created1); + assertNotNull(created2); + assertNotNull(created3); + + // Enable dashboard config for site + defaultDashboardConfigService.enableForSite(created1.getId()); + defaultDashboardConfigService.enableForSite(created2.getId()); + defaultDashboardConfigService.enableForSite(created3.getId()); + + commitTransaction(); + + // Verify enabled dashboard configs + Optional retrieved1 = defaultDashboardConfigService.retrieve(created1.getId()); + Optional retrieved2 = defaultDashboardConfigService.retrieve(created2.getId()); + Optional retrieved3 = defaultDashboardConfigService.retrieve(created3.getId()); + + assertTrue(retrieved1.isPresent()); + assertTrue(retrieved2.isPresent()); + assertTrue(retrieved3.isPresent()); + + assertTrue(retrieved1.get().getScopes().get(Scope.Site).isEnabled()); + assertTrue(retrieved2.get().getScopes().get(Scope.Site).isEnabled()); + assertTrue(retrieved3.get().getScopes().get(Scope.Site).isEnabled()); + } + + @Test + @DirtiesContext + public void test_disableForSite() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard configs + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Verify created dashboard configs + assertNotNull(created1); + assertNotNull(created2); + assertNotNull(created3); + + // Disable dashboard config for site + defaultDashboardConfigService.disableForSite(created1.getId()); + defaultDashboardConfigService.disableForSite(created2.getId()); + defaultDashboardConfigService.disableForSite(created3.getId()); + + commitTransaction(); + + // Verify disabled dashboard configs + Optional retrieved1 = defaultDashboardConfigService.retrieve(created1.getId()); + Optional retrieved2 = defaultDashboardConfigService.retrieve(created2.getId()); + Optional retrieved3 = defaultDashboardConfigService.retrieve(created3.getId()); + + assertTrue(retrieved1.isPresent()); + assertTrue(retrieved2.isPresent()); + assertTrue(retrieved3.isPresent()); + + assertFalse(retrieved1.get().getScopes().get(Scope.Site).isEnabled()); + assertFalse(retrieved2.get().getScopes().get(Scope.Site).isEnabled()); + assertFalse(retrieved3.get().getScopes().get(Scope.Site).isEnabled()); + } + + @Test + @DirtiesContext + public void test_enable_and_disableForProject() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard configs + DashboardConfig created1 = defaultDashboardConfigService.create(dashboardConfig1); + DashboardConfig created2 = defaultDashboardConfigService.create(dashboardConfig2); + DashboardConfig created3 = defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Verify created dashboard configs + assertNotNull(created1); + assertNotNull(created2); + assertNotNull(created3); + + // Enable dashboard config for project + defaultDashboardConfigService.enableForProject(created1.getId(), "ProjectA"); + defaultDashboardConfigService.enableForProject(created1.getId(), "ProjectB"); + defaultDashboardConfigService.enableForProject(created1.getId(), "ProjectC"); + defaultDashboardConfigService.enableForProject(created2.getId(), "ProjectA"); + defaultDashboardConfigService.enableForProject(created2.getId(), "ProjectB"); + defaultDashboardConfigService.enableForProject(created2.getId(), "ProjectC"); + defaultDashboardConfigService.enableForProject(created3.getId(), "ProjectA"); + defaultDashboardConfigService.enableForProject(created3.getId(), "ProjectB"); + defaultDashboardConfigService.enableForProject(created3.getId(), "ProjectC"); + commitTransaction(); + + // Verify enabled dashboard configs + Optional retrieved1 = defaultDashboardConfigService.retrieve(created1.getId()); + Optional retrieved2 = defaultDashboardConfigService.retrieve(created2.getId()); + Optional retrieved3 = defaultDashboardConfigService.retrieve(created3.getId()); + + assertTrue(retrieved1.isPresent()); + assertTrue(retrieved2.isPresent()); + assertTrue(retrieved3.isPresent()); + + assertThat(retrieved1.get().getScopes().get(Scope.Project).getIds(), hasItems("ProjectA", "ProjectB", "ProjectC")); + assertThat(retrieved2.get().getScopes().get(Scope.Project).getIds(), hasItems("ProjectA", "ProjectB", "ProjectC")); + assertThat(retrieved3.get().getScopes().get(Scope.Project).getIds(), hasItems("ProjectA", "ProjectB", "ProjectC")); + + // Disable dashboard config for project + // Disable dashboard config for project + defaultDashboardConfigService.disableForProject(created1.getId(), "ProjectA"); + defaultDashboardConfigService.disableForProject(created1.getId(), "ProjectB"); + defaultDashboardConfigService.disableForProject(created1.getId(), "ProjectC"); + defaultDashboardConfigService.disableForProject(created2.getId(), "ProjectA"); + defaultDashboardConfigService.disableForProject(created2.getId(), "ProjectB"); + defaultDashboardConfigService.disableForProject(created2.getId(), "ProjectC"); + defaultDashboardConfigService.disableForProject(created3.getId(), "ProjectA"); + defaultDashboardConfigService.disableForProject(created3.getId(), "ProjectB"); + defaultDashboardConfigService.disableForProject(created3.getId(), "ProjectC"); + commitTransaction(); + + // Verify disabled dashboard configs + retrieved1 = defaultDashboardConfigService.retrieve(created1.getId()); + retrieved2 = defaultDashboardConfigService.retrieve(created2.getId()); + retrieved3 = defaultDashboardConfigService.retrieve(created3.getId()); + + assertTrue(retrieved1.isPresent()); + assertTrue(retrieved2.isPresent()); + assertTrue(retrieved3.isPresent()); + + assertThat(retrieved1.get().getScopes().get(Scope.Project).getIds(), not(hasItems("ProjectA", "ProjectB", "ProjectC"))); + assertThat(retrieved2.get().getScopes().get(Scope.Project).getIds(), not(hasItems("ProjectA", "ProjectB", "ProjectC"))); + assertThat(retrieved3.get().getScopes().get(Scope.Project).getIds(), not(hasItems("ProjectA", "ProjectB", "ProjectC"))); + } + + @Test(expected = DataIntegrityViolationException.class) + @DirtiesContext + public void testCantDeleteFrameworkInUse() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard configs + defaultDashboardConfigService.create(dashboardConfig1); + defaultDashboardConfigService.create(dashboardConfig2); + defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Try to delete dashboard framework in use + dashboardFrameworkService.delete(panel.getName()); + commitTransaction(); + fail("Should not be able to delete dashboard framework in use by dashboard configs"); + } + + @Test + @DirtiesContext + public void testCantDeleteComputeEnvironmentConfigInUse() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard configs + defaultDashboardConfigService.create(dashboardConfig1); + defaultDashboardConfigService.create(dashboardConfig2); + defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Try to delete compute environment config in use + try { + computeEnvironmentConfigService.delete(computeEnvironmentConfig1.getId()); + } catch (RuntimeException e) { + assertTrue(computeEnvironmentConfigService.exists(computeEnvironmentConfig1.getId())); + return; + } + + fail("Should not be able to delete compute environment config in use by dashboard configs"); + } + + @Test + @DirtiesContext + public void testCanDeleteComputeEnvironmentConfigNotInUse() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard configs + defaultDashboardConfigService.create(dashboardConfig1); + defaultDashboardConfigService.create(dashboardConfig2); + defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Try to delete compute environment config not in use + computeEnvironmentConfigService.delete(computeEnvironmentConfig2.getId()); + commitTransaction(); + assertFalse(computeEnvironmentConfigService.exists(computeEnvironmentConfig2.getId())); + } + + @Test + @DirtiesContext + public void testCantDeleteHardwareConfigInUse() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard configs + defaultDashboardConfigService.create(dashboardConfig1); + defaultDashboardConfigService.create(dashboardConfig2); + defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Try to delete hardware config in use + try { + hardwareConfigService.delete(hardwareConfig1.getId()); + } catch (RuntimeException e) { + assertTrue(hardwareConfigService.exists(hardwareConfig1.getId())); + return; + } + + fail("Should not be able to delete hardware config in use by dashboard configs"); + } + + @Test + @DirtiesContext + public void testCanDeleteHardwareConfigNotInUse() throws Exception { + // Create compute environment and hardware configs + commitComputeEnvironmentAndHardwareConfigs(); + + // Commit dashboard frameworks + commitDashboardFrameworks(); + + // Create dashboard configs + defaultDashboardConfigService.create(dashboardConfig1); + defaultDashboardConfigService.create(dashboardConfig2); + defaultDashboardConfigService.create(dashboardConfig3); + + commitTransaction(); + + // Try to delete hardware config not in use + hardwareConfigService.delete(hardwareConfig2.getId()); + commitTransaction(); + assertFalse(hardwareConfigService.exists(hardwareConfig2.getId())); + } + + public void createDummyConfigs() { + // Setup hardware + Hardware hardware1 = Hardware.builder() + .name("Small") + .cpuReservation(2.0) + .cpuLimit(4.0) + .memoryReservation("4G") + .memoryLimit("8G") + .build(); + + // Setup hardware constraints + Constraint hardwareConstraint1 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Collections.singletonList("worker"))) + .build(); + + Constraint hardwareConstraint2 = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + hardware1.setConstraints(Arrays.asList(hardwareConstraint1, hardwareConstraint2)); + + // Setup hardware environment variables + EnvironmentVariable hardwareEnvironmentVariable1 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); + EnvironmentVariable hardwareEnvironmentVariable2 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); + + hardware1.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable1, hardwareEnvironmentVariable2)); + + // Setup hardware generic resources + GenericResource hardwareGenericResource1 = new GenericResource("nvidia.com/gpu", "2"); + GenericResource hardwareGenericResource2 = new GenericResource("fpga.com/fpga", "1"); + + hardware1.setGenericResources(Arrays.asList(hardwareGenericResource1, hardwareGenericResource2)); + + // Setup hardware scopes + HardwareScope hardwareSiteScope1 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope1 = HardwareScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope userHardwareScope1 = HardwareScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map hardwareScopes1 = new HashMap<>(); + hardwareScopes1.put(Site, hardwareSiteScope1); + hardwareScopes1.put(Project, hardwareProjectScope1); + hardwareScopes1.put(User, userHardwareScope1); + + // Setup a hardware config entity + hardwareConfig1 = HardwareConfig.builder() + .hardware(hardware1) + .scopes(hardwareScopes1) + .build(); + + // Setup second hardware config + Hardware hardware2 = Hardware.builder() + .name("Medium") + .cpuReservation(4.0) + .cpuLimit(4.0) + .memoryReservation("8G") + .memoryLimit("8G") + .build(); + + // Setup hardware constraints + Constraint hardwareConstraint3 = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Collections.singletonList("worker"))) + .build(); + + Constraint hardwareConstraint4 = Constraint.builder() + .key("node.instance.type") + .operator(Constraint.Operator.NOT_IN) + .values(new HashSet<>(Arrays.asList("spot", "demand"))) + .build(); + + hardware2.setConstraints(Arrays.asList(hardwareConstraint3, hardwareConstraint4)); + + // Setup hardware environment variables + EnvironmentVariable hardwareEnvironmentVariable3 = new EnvironmentVariable("MATLAB_LICENSE_FILE", "12345@myserver"); + EnvironmentVariable hardwareEnvironmentVariable4 = new EnvironmentVariable("NVIDIA_VISIBLE_DEVICES", "all"); + + hardware2.setEnvironmentVariables(Arrays.asList(hardwareEnvironmentVariable3, hardwareEnvironmentVariable4)); + + // Setup hardware generic resources + GenericResource hardwareGenericResource3 = new GenericResource("nvidia.com/gpu", "2"); + GenericResource hardwareGenericResource4 = new GenericResource("fpga.com/fpga", "1"); + + hardware2.setGenericResources(Arrays.asList(hardwareGenericResource3, hardwareGenericResource4)); + + // Setup hardware scopes + HardwareScope hardwareSiteScope2 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope2 = HardwareScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope userHardwareScope2 = HardwareScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map hardwareScopes2 = new HashMap<>(); + hardwareScopes2.put(Site, hardwareSiteScope2); + hardwareScopes2.put(Project, hardwareProjectScope2); + hardwareScopes2.put(User, userHardwareScope2); + + // Setup second hardware config entity + hardwareConfig2 = HardwareConfig.builder() + .hardware(hardware2) + .scopes(hardwareScopes2) + .build(); + + // Setup second hardware config + Hardware hardware3 = Hardware.builder() + .name("Large") + .cpuReservation(8.0) + .cpuLimit(8.0) + .memoryReservation("16G") + .memoryLimit("16G") + .build(); + + // No constraints, environment variables or generic resources + hardware3.setConstraints(Collections.emptyList()); + hardware3.setEnvironmentVariables(Collections.emptyList()); + hardware3.setGenericResources(Collections.emptyList()); + + // Setup hardware scopes + HardwareScope hardwareSiteScope3 = HardwareScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + HardwareScope hardwareProjectScope3 = HardwareScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("ProjectABCDE"))) + .build(); + + HardwareScope userHardwareScope3 = HardwareScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map hardwareScopes3 = new HashMap<>(); + hardwareScopes3.put(Site, hardwareSiteScope3); + hardwareScopes3.put(Project, hardwareProjectScope3); + hardwareScopes3.put(User, userHardwareScope3); + + // Setup second hardware config entity + hardwareConfig3 = HardwareConfig.builder() + .hardware(hardware3) + .scopes(hardwareScopes3) + .build(); + + // Setup first compute environment + ComputeEnvironment computeEnvironment1 = ComputeEnvironment.builder() + .name("Jupyter Datascience Notebook") + .image("jupyter/datascience-notebook:hub-3.0.0") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeEnvironmentScope computeEnvironmentSiteScope1 = ComputeEnvironmentScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeEnvironmentScope computeEnvironmentProjectScope1 = ComputeEnvironmentScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeEnvironmentScope computeEnvironmentUserScope1 = ComputeEnvironmentScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + Map computeEnvironmentScopes1 = new HashMap<>(); + computeEnvironmentScopes1.put(Site, computeEnvironmentSiteScope1); + computeEnvironmentScopes1.put(Project, computeEnvironmentProjectScope1); + computeEnvironmentScopes1.put(User, computeEnvironmentUserScope1); + + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions1 = ComputeEnvironmentHardwareOptions.builder() + .allowAllHardware(true) + .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig1, hardwareConfig2, hardwareConfig3))) + .build(); + + computeEnvironmentConfig1 = ComputeEnvironmentConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(ComputeEnvironmentConfig.ConfigType.JUPYTERHUB))) + .computeEnvironment(computeEnvironment1) + .scopes(computeEnvironmentScopes1) + .hardwareOptions(computeEnvironmentHardwareOptions1) + .build(); + + // Setup second compute environment + ComputeEnvironment computeEnvironment2 = ComputeEnvironment.builder() + .name("XNAT Datascience Notebook") + .image("xnat/datascience-notebook:latest") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeEnvironmentScope computeEnvironmentSiteScope2 = ComputeEnvironmentScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeEnvironmentScope computeEnvironmentProjectScope2 = ComputeEnvironmentScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(new ArrayList<>(Arrays.asList("Project1", "Project10", "Project100")))) + .build(); + + ComputeEnvironmentScope computeEnvironmentUserScope2 = ComputeEnvironmentScope.builder() + .scope(User) + .enabled(false) + .ids(new HashSet<>(new ArrayList<>(Arrays.asList("User1", "User10", "User100")))) + .build(); + + ComputeEnvironmentScope computeEnvironmentDatatypeScope2 = ComputeEnvironmentScope.builder() + .scope(DataType) + .enabled(false) + .ids(new HashSet<>(Arrays.asList("xnat:mrSessionData", "xnat:petSessionData", "xnat:ctSessionData"))) + .build(); + + Map computeEnvironmentScopes2 = new HashMap<>(); + computeEnvironmentScopes2.put(Site, computeEnvironmentSiteScope2); + computeEnvironmentScopes2.put(Project, computeEnvironmentProjectScope2); + computeEnvironmentScopes2.put(User, computeEnvironmentUserScope2); + computeEnvironmentScopes2.put(DataType, computeEnvironmentDatatypeScope2); + + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions2 = ComputeEnvironmentHardwareOptions.builder() + .allowAllHardware(false) + .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig2, hardwareConfig3))) + .build(); + + computeEnvironmentConfig2 = ComputeEnvironmentConfig.builder() + .configTypes(new HashSet<>(Collections.singletonList(CONTAINER_SERVICE))) + .computeEnvironment(computeEnvironment2) + .scopes(computeEnvironmentScopes2) + .hardwareOptions(computeEnvironmentHardwareOptions2) + .build(); + + // Setup third compute environment + ComputeEnvironment computeEnvironment3 = ComputeEnvironment.builder() + .name("MATLAB Datascience Notebook") + .image("matlab/datascience-notebook:latest") + .environmentVariables(new ArrayList<>()) + .mounts(new ArrayList<>()) + .build(); + + ComputeEnvironmentScope computeEnvironmentSiteScope3 = ComputeEnvironmentScope.builder() + .scope(Site) + .enabled(false) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + ComputeEnvironmentScope computeEnvironmentProjectScope3 = ComputeEnvironmentScope.builder() + .scope(Project) + .enabled(true) + .ids(new HashSet<>()) + .build(); + + ComputeEnvironmentScope computeEnvironmentUserScope3 = ComputeEnvironmentScope.builder() + .scope(User) + .enabled(true) + .ids(new HashSet<>()) + .build(); + + Map computeEnvironmentScopes3 = new HashMap<>(); + computeEnvironmentScopes3.put(Site, computeEnvironmentSiteScope3); + computeEnvironmentScopes3.put(Project, computeEnvironmentProjectScope3); + computeEnvironmentScopes3.put(User, computeEnvironmentUserScope3); + + ComputeEnvironmentHardwareOptions computeEnvironmentHardwareOptions3 = ComputeEnvironmentHardwareOptions.builder() + .allowAllHardware(false) + .hardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig2, hardwareConfig3))) + .build(); + + computeEnvironmentConfig3 = ComputeEnvironmentConfig.builder() + .configTypes(new HashSet<>(Arrays.asList(CONTAINER_SERVICE, JUPYTERHUB))) + .computeEnvironment(computeEnvironment3) + .scopes(computeEnvironmentScopes3) + .hardwareOptions(computeEnvironmentHardwareOptions3) + .build(); + + // Setup dashboard frameworks + panel = DashboardFramework.builder() + .name("Panel") + .commandTemplate("jhsingle-native-proxy ...") + .build(); + streamlit = DashboardFramework.builder() + .name("Streamlit") + .commandTemplate("jhsingle-native-proxy ...") + .build(); + + voila = DashboardFramework.builder() + .name("Voila") + .commandTemplate("jhsingle-native-proxy ...") + .build(); + + dash = DashboardFramework.builder() + .name("Dash") + .commandTemplate("jhsingle-native-proxy ...") + .build(); + + List frameworks = Arrays.asList(panel, streamlit, voila, dash); + + // Setup first dashboard config + Dashboard dashboard1 = Dashboard.builder() + .name("Panel Dashboard") + .description("A dashboard using Panel") + .framework("Panel") + .command(null) + .fileSource("git") + .gitRepoUrl("https://github.com/andylassiter/dashboard-testing.git") + .gitRepoBranch("main") + .mainFilePath("pane/panel_dashboard.ipynb") + .build(); + + DashboardScope dashboardSiteScope1 = DashboardScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + DashboardScope dashboardProjectScope1 = DashboardScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(new ArrayList<>(Arrays.asList("Project1", "Project2", "Project3")))) + .build(); + + DashboardScope dashboardDatatypeScope1 = DashboardScope.builder() + .scope(DataType) + .enabled(false) + .ids(new HashSet<>(Arrays.asList("xnat:mrSessionData", "xnat:petSessionData", "xnat:ctSessionData", "xnat:projectData"))) + .build(); + + Map dashboardScopes1 = new HashMap<>(); + dashboardScopes1.put(Site, dashboardSiteScope1); + dashboardScopes1.put(Project, dashboardProjectScope1); + dashboardScopes1.put(DataType, dashboardDatatypeScope1); + + dashboardConfig1 = DashboardConfig.builder() + .dashboard(dashboard1) + .scopes(dashboardScopes1) + .computeEnvironmentConfig(computeEnvironmentConfig1) + .hardwareConfig(hardwareConfig1) + .build(); + + // Setup second dashboard config + Dashboard dashboard2 = Dashboard.builder() + .name("Voila Dashboard") + .description("A dashboard using Voila") + .framework("Voila") + .command(null) + .fileSource("git") + .gitRepoUrl("https://github.com/andylassiter/dashboard-testing.git") + .gitRepoBranch("main") + .mainFilePath("voila/voila_dashboard.ipynb") + .build(); + + DashboardScope dashboardSiteScope2 = DashboardScope.builder() + .scope(Site) + .enabled(true) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + DashboardScope dashboardProjectScope2 = DashboardScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(new ArrayList<>(Collections.singletonList("Project1")))) + .build(); + + DashboardScope dashboardDatatypeScope2 = DashboardScope.builder() + .scope(DataType) + .enabled(false) + .ids(new HashSet<>(Collections.singletonList("xnat:projectData"))) + .build(); + + Map dashboardScopes2 = new HashMap<>(); + dashboardScopes2.put(Site, dashboardSiteScope2); + dashboardScopes2.put(Project, dashboardProjectScope2); + dashboardScopes2.put(DataType, dashboardDatatypeScope2); + + dashboardConfig2 = DashboardConfig.builder() + .dashboard(dashboard2) + .scopes(dashboardScopes2) + .computeEnvironmentConfig(computeEnvironmentConfig3) + .hardwareConfig(hardwareConfig2) + .build(); + + // Setup third dashboard config + Dashboard dashboard3 = Dashboard.builder() + .name("Streamlit Dashboard") + .description("A dashboard using Streamlit") + .framework("Streamlit") + .command(null) + .fileSource("git") + .gitRepoUrl("https://github.com/andylassiter/dashboard-testing.git") + .gitRepoBranch("main") + .mainFilePath("streamlit/streamlit_dashboard.py") + .build(); + + DashboardScope dashboardSiteScope3 = DashboardScope.builder() + .scope(Site) + .enabled(false) + .ids(new HashSet<>(Collections.emptyList())) + .build(); + + DashboardScope dashboardProjectScope3 = DashboardScope.builder() + .scope(Project) + .enabled(false) + .ids(new HashSet<>(new ArrayList<>(Collections.singletonList("Project1")))) + .build(); + + DashboardScope dashboardDatatypeScope3 = DashboardScope.builder() + .scope(DataType) + .enabled(false) + .ids(new HashSet<>(Arrays.asList("xnat:projectData", "xnat:subjectData"))) + .build(); + + Map dashboardScopes3 = new HashMap<>(); + dashboardScopes3.put(Site, dashboardSiteScope3); + dashboardScopes3.put(Project, dashboardProjectScope3); + dashboardScopes3.put(DataType, dashboardDatatypeScope3); + + dashboardConfig3 = DashboardConfig.builder() + .dashboard(dashboard3) + .scopes(dashboardScopes3) + .computeEnvironmentConfig(computeEnvironmentConfig3) + .hardwareConfig(hardwareConfig3) + .build(); + } + + public void commitDashboardFrameworks() { + panel = dashboardFrameworkService.create(panel); + streamlit = dashboardFrameworkService.create(streamlit); + voila = dashboardFrameworkService.create(voila); + dash = dashboardFrameworkService.create(dash); + + commitTransaction(); + } + + public void commitComputeEnvironmentAndHardwareConfigs() { + // First create hardware configs + hardwareConfig1 = hardwareConfigService.create(hardwareConfig1); + hardwareConfig2 = hardwareConfigService.create(hardwareConfig2); + hardwareConfig3 = hardwareConfigService.create(hardwareConfig3); + + commitTransaction(); + + // Then create compute environment configs + computeEnvironmentConfig1 = computeEnvironmentConfigService.create(computeEnvironmentConfig1); + computeEnvironmentConfig2 = computeEnvironmentConfigService.create(computeEnvironmentConfig2); + computeEnvironmentConfig3 = computeEnvironmentConfigService.create(computeEnvironmentConfig3); + + commitTransaction(); + + // The dashboard configs need to be updated with the new compute environment and hardware configs (for the ids) + dashboardConfig1.setComputeEnvironmentConfig(computeEnvironmentConfig1); + dashboardConfig1.setHardwareConfig(hardwareConfig1); + + dashboardConfig2.setComputeEnvironmentConfig(computeEnvironmentConfig3); + dashboardConfig2.setHardwareConfig(hardwareConfig3); + + dashboardConfig3.setComputeEnvironmentConfig(computeEnvironmentConfig3); + dashboardConfig3.setHardwareConfig(hardwareConfig3); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardFrameworkServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardFrameworkServiceTest.java new file mode 100644 index 0000000..3382d18 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardFrameworkServiceTest.java @@ -0,0 +1,238 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.nrg.xnatx.plugins.jupyterhub.config.DefaultDashboardFrameworkServiceTestConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.Dashboard; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardFramework; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.transaction.Transactional; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.nrg.xnatx.plugins.jupyterhub.utils.TestingUtils.commitTransaction; + +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +@ContextConfiguration(classes = DefaultDashboardFrameworkServiceTestConfig.class) +public class DefaultDashboardFrameworkServiceTest { + + @Autowired private DefaultDashboardFrameworkService service; + @Autowired private DashboardFrameworkEntityService entityService; + + private DashboardFramework panel; + private DashboardFramework streamlit; + private List frameworks; + + @Before + public void setup() { + panel = DashboardFramework.builder() + .name("Panel") + .commandTemplate("jhsingle-native-proxy --destport {port} --repo {repo} --repobranch {repobranch} --repofolder /home/jovyan/work bokeh_root_cmd.main /home/jovyan/work/{mainFilePath}") + .build(); + streamlit = DashboardFramework.builder() + .name("Streamlit") + .commandTemplate("jhsingle-native-proxy --destport {port} --authtype none --user {username} --group {group} --debug") + .build(); + + frameworks = Arrays.asList(panel, streamlit); + } + + @After + public void after() throws Exception { + } + + @Test + public void test() { + assertNotNull(service); + assertNotNull(entityService); + } + + @Test + @DirtiesContext + public void testCreate() { + // Setup and execute + List created = commitDashboardFrameworks(); + + // Verify + assertThat(created.size(), is(frameworks.size())); + for (DashboardFramework framework : created) { + assertThat(frameworks.contains(framework), is(true)); + } + } + + @Test + @DirtiesContext + public void testUpdate() throws Exception { + // Setup + List created = commitDashboardFrameworks(); + + // Execute + List updated = new ArrayList<>(); + for (DashboardFramework framework : created) { + framework.setCommandTemplate("jhsingle-native-proxy --destport {port} --authtype none --user {username} --group {group} --debug"); + updated.add(service.update(framework)); + } + + commitTransaction(); + + // Verify + assertThat(updated.size(), is(created.size())); + for (DashboardFramework framework : updated) { + assertThat(created.contains(framework), is(true)); + } + } + + @Test + @DirtiesContext + public void testGet() throws Exception { + // Setup + List created = commitDashboardFrameworks(); + + // Execute + List retrieved = new ArrayList<>(); + for (DashboardFramework framework : created) { + retrieved.add(service.get(framework.getId()).get()); + } + + // Verify + assertThat(retrieved.size(), is(created.size())); + for (DashboardFramework framework : retrieved) { + assertThat(created.contains(framework), is(true)); + } + } + + @Test + @DirtiesContext + public void testGetByName() throws Exception { + // Setup + List created = commitDashboardFrameworks(); + + // Execute + List retrieved = new ArrayList<>(); + for (DashboardFramework framework : created) { + retrieved.add(service.get(framework.getName()).get()); + } + + // Verify + assertThat(retrieved.size(), is(created.size())); + for (DashboardFramework framework : retrieved) { + assertThat(created.contains(framework), is(true)); + } + } + + @Test + @DirtiesContext + public void testGetAll() throws Exception { + // Setup + List created = commitDashboardFrameworks(); + + // Execute + List retrieved = service.getAll(); + + // Verify + assertThat(retrieved.size(), is(created.size())); + for (DashboardFramework framework : retrieved) { + assertThat(created.contains(framework), is(true)); + } + } + + @Test + @DirtiesContext + public void testDelete() throws Exception { + // Setup + List created = commitDashboardFrameworks(); + + // Execute + for (DashboardFramework framework : created) { + service.delete(framework.getId()); + } + + commitTransaction(); + + // Verify + assertThat(service.getAll().size(), is(0)); + } + + @Test + @DirtiesContext + public void testDeleteByName() throws Exception { + // Setup + List created = commitDashboardFrameworks(); + + // Execute + for (DashboardFramework framework : created) { + service.delete(framework.getName()); + } + + commitTransaction(); + + // Verify + assertThat(service.getAll().size(), is(0)); + } + + @Test + @DirtiesContext + public void test_resolve() { + // Setup + Dashboard dashboard = Dashboard.builder() + .name("Test Dashboard") + .description("Test dashboard description") + .framework("Panel") + .command("") + .fileSource("git") + .gitRepoUrl("https://github.com/andylassiter/dashboard-testing.git") + .gitRepoBranch("main") + .mainFilePath("panel/subject-demographics.py") + .build(); + + commitDashboardFrameworks(); + + // Execute + String command = service.resolveCommand(dashboard); + + // Verify + assertThat(command, containsString(dashboard.getGitRepoUrl())); + assertThat(command, containsString(dashboard.getGitRepoBranch())); + assertThat(command, containsString(dashboard.getMainFilePath())); + } + + @Test + public void test_removeExtraSpaces() { + // Setup + String command = "jhsingle-native-proxy --destport {port} --repo {repo} --repobranch {repobranch} --repofolder /home/jovyan/work bokeh_root_cmd.main /home/jovyan/work/{mainFilePath}"; + String expected = "jhsingle-native-proxy --destport {port} --repo {repo} --repobranch {repobranch} --repofolder /home/jovyan/work bokeh_root_cmd.main /home/jovyan/work/{mainFilePath}"; + + // Execute + String result = service.removeExtraSpaces(command); + + // Verify + assertThat(result, is(expected)); + } + + public List commitDashboardFrameworks() { + List created = new ArrayList<>(); + for (DashboardFramework framework : frameworks) { + DashboardFramework temp = service.create(framework); + framework.setId(temp.getId()); + created.add(temp); + } + + commitTransaction(); + + return created; + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardJobTemplateServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardJobTemplateServiceTest.java new file mode 100644 index 0000000..d3fb85f --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultDashboardJobTemplateServiceTest.java @@ -0,0 +1,187 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.xnat.compute.models.*; +import org.nrg.xnat.compute.services.ComputeEnvironmentConfigService; +import org.nrg.xnat.compute.services.ConstraintConfigService; +import org.nrg.xnat.compute.services.HardwareConfigService; +import org.nrg.xnatx.plugins.jupyterhub.config.DefaultDashboardJobTemplateServiceTestConfig; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardConfig; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardConfigService; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardFrameworkService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = DefaultDashboardJobTemplateServiceTestConfig.class) +public class DefaultDashboardJobTemplateServiceTest { + + @Autowired private ComputeEnvironmentConfigService mockComputeEnvironmentConfigService; + @Autowired private HardwareConfigService mockHardwareConfigService; + @Autowired private ConstraintConfigService mockConstraintConfigService; + @Autowired private DashboardConfigService mockDashboardConfigService; + @Autowired private DashboardFrameworkService mockDashboardFrameworkService; + @Autowired private DefaultDashboardJobTemplateService defaultDashboardJobTemplateService; + + @Before + public void before() { + } + + @After + public void after() { + Mockito.reset( + mockComputeEnvironmentConfigService, + mockHardwareConfigService, + mockConstraintConfigService + ); + } + + @Test + public void test_wiring() { + assertNotNull(mockComputeEnvironmentConfigService); + assertNotNull(mockHardwareConfigService); + assertNotNull(mockConstraintConfigService); + assertNotNull(mockDashboardConfigService); + assertNotNull(mockDashboardFrameworkService); + assertNotNull(defaultDashboardJobTemplateService); + } + + @Test + public void test_isCompEnvAndHardwareAvailable() { + // Run + boolean isAvailable = defaultDashboardJobTemplateService.isAvailable(1L, 1L, null); + + // Verify + assertTrue(isAvailable); + } + + @Test + public void test_DashboardCompEnvAndHardwareUnavailable_1() { + // Setup + when(mockDashboardConfigService.isAvailable(any(), any())).thenReturn(false); + + // Run + boolean isAvailable = defaultDashboardJobTemplateService.isAvailable(1L, 1L, null); + + // Verify + assertTrue(isAvailable); + } + + + @Test + public void test_DashboardCompEnvAndHardwareAvailable() { + // Setup + DashboardConfig dashboardConfig = DashboardConfig.builder().id(1L).build(); + + when(mockDashboardConfigService.isAvailable(any(), any())).thenReturn(true); + when(mockDashboardConfigService.retrieve(1L)).thenReturn(Optional.of(dashboardConfig)); + + + ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().id(1L).build(); + ComputeEnvironment computeEnvironment = ComputeEnvironment.builder() + .name("JupyterHub Data Science Notebook") + .image("jupyter/datascience-notebook:latest") + .build(); + computeEnvironmentConfig.setComputeEnvironment(computeEnvironment); + ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); + hardwareOptions.setAllowAllHardware(false); + computeEnvironmentConfig.setHardwareOptions(hardwareOptions); + HardwareConfig hardwareConfig = HardwareConfig.builder().id(1L).build(); + Hardware hardware = Hardware.builder() + .name("Standard") + .cpuLimit(1.0) + .memoryLimit("4G") + .build(); + hardwareConfig.setHardware(hardware); + hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); + + Constraint constraint = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Arrays.asList("worker"))) + .build(); + + ConstraintConfig constraintConfig = ConstraintConfig.builder() + .id(1L) + .constraint(constraint) + .build(); + + dashboardConfig.setComputeEnvironmentConfig(computeEnvironmentConfig); + dashboardConfig.setHardwareConfig(hardwareConfig); + + when(mockComputeEnvironmentConfigService.retrieve(1L)).thenReturn(Optional.of(computeEnvironmentConfig)); + when(mockHardwareConfigService.retrieve(1L)).thenReturn(Optional.of(hardwareConfig)); + when(mockConstraintConfigService.retrieve(1L)).thenReturn(Optional.of(constraintConfig)); + + // Run + boolean isAvailable = defaultDashboardJobTemplateService.isAvailable(1L, 1L, 1L, null); + + // Verify + assertTrue(isAvailable); + } + + @Test + public void test_ResolveDashboardConfig() { + // Setup + DashboardConfig dashboardConfig = DashboardConfig.builder().id(1L).build(); + + when(mockDashboardConfigService.isAvailable(any(), any())).thenReturn(true); + when(mockDashboardConfigService.retrieve(1L)).thenReturn(Optional.of(dashboardConfig)); + + + ComputeEnvironmentConfig computeEnvironmentConfig = ComputeEnvironmentConfig.builder().id(1L).build(); + ComputeEnvironment computeEnvironment = ComputeEnvironment.builder() + .name("JupyterHub Data Science Notebook") + .image("jupyter/datascience-notebook:latest") + .build(); + computeEnvironmentConfig.setComputeEnvironment(computeEnvironment); + ComputeEnvironmentHardwareOptions hardwareOptions = ComputeEnvironmentHardwareOptions.builder().build(); + hardwareOptions.setAllowAllHardware(false); + computeEnvironmentConfig.setHardwareOptions(hardwareOptions); + HardwareConfig hardwareConfig = HardwareConfig.builder().id(1L).build(); + Hardware hardware = Hardware.builder() + .name("Standard") + .cpuLimit(1.0) + .memoryLimit("4G") + .build(); + hardwareConfig.setHardware(hardware); + hardwareOptions.setHardwareConfigs(new HashSet<>(Arrays.asList(hardwareConfig))); + + Constraint constraint = Constraint.builder() + .key("node.role") + .operator(Constraint.Operator.IN) + .values(new HashSet<>(Arrays.asList("worker"))) + .build(); + + ConstraintConfig constraintConfig = ConstraintConfig.builder() + .id(1L) + .constraint(constraint) + .build(); + + dashboardConfig.setComputeEnvironmentConfig(computeEnvironmentConfig); + dashboardConfig.setHardwareConfig(hardwareConfig); + + when(mockComputeEnvironmentConfigService.retrieve(1L)).thenReturn(Optional.of(computeEnvironmentConfig)); + when(mockHardwareConfigService.retrieve(1L)).thenReturn(Optional.of(hardwareConfig)); + when(mockConstraintConfigService.retrieve(1L)).thenReturn(Optional.of(constraintConfig)); + + // Run + JobTemplate jobTemplate = defaultDashboardJobTemplateService.resolve(1L, 1L, 1L, Collections.emptyMap()); + + } +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java index d72a355..e60a36e 100644 --- a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/DefaultJupyterHubServiceTest.java @@ -8,7 +8,6 @@ import org.mockito.Captor; import org.mockito.Mockito; import org.nrg.framework.services.NrgEventServiceI; -import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xdat.om.XnatExperimentdata; import org.nrg.xdat.om.XnatImagescandata; import org.nrg.xdat.om.XnatProjectdata; @@ -16,9 +15,8 @@ import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xdat.security.user.exceptions.UserInitException; import org.nrg.xft.security.UserI; +import org.nrg.xnat.compute.services.JobTemplateService; import org.nrg.xnatx.plugins.jupyterhub.client.JupyterHubClient; -import org.nrg.xnatx.plugins.jupyterhub.client.exceptions.ResourceAlreadyExistsException; -import org.nrg.xnatx.plugins.jupyterhub.client.exceptions.UserNotFoundException; import org.nrg.xnatx.plugins.jupyterhub.client.models.Server; import org.nrg.xnatx.plugins.jupyterhub.client.models.Token; import org.nrg.xnatx.plugins.jupyterhub.client.models.User; @@ -27,6 +25,7 @@ import org.nrg.xnatx.plugins.jupyterhub.events.JupyterServerEventI; import org.nrg.xnatx.plugins.jupyterhub.models.ServerStartRequest; import org.nrg.xnatx.plugins.jupyterhub.preferences.JupyterHubPreferences; +import org.nrg.xnatx.plugins.jupyterhub.services.DashboardJobTemplateService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsEntityService; import org.nrg.xnatx.plugins.jupyterhub.services.UserOptionsService; import org.nrg.xnatx.plugins.jupyterhub.utils.PermissionsHelper; @@ -58,6 +57,7 @@ public class DefaultJupyterHubServiceTest { @Autowired private JupyterHubPreferences mockJupyterHubPreferences; @Autowired private UserManagementServiceI mockUserManagementServiceI; @Autowired private JobTemplateService mockJobTemplateService; + @Autowired private DashboardJobTemplateService mockDashboardJobTemplateService; @Captor ArgumentCaptor jupyterServerEventCaptor; @Captor ArgumentCaptor tokenArgumentCaptor; @@ -169,6 +169,7 @@ public void after() { Mockito.reset(mockPermissionsHelper); Mockito.reset(mockUserOptionsEntityService); Mockito.reset(mockUserManagementServiceI); + Mockito.reset(mockJobTemplateService); } @Test @@ -419,6 +420,32 @@ public void testStartServer_jobTemplateUnavailable() throws Exception { verify(mockJupyterHubClient, never()).startServer(any(), any(), any()); } + @Test(timeout = 2000) + public void testStartServer_dashboardJobTemplateUnavailable() throws Exception { + // Grant permissions + when(mockPermissionsHelper.canRead(any(), anyString(), anyString(), anyString())).thenReturn(true); + + // Dashboard job template unavailable + when(mockDashboardJobTemplateService.isAvailable(any(), any(), any(), any())).thenReturn(false); + + // Test + startProjectRequest.setDashboardConfigId(1L); + jupyterHubService.startServer(user, startProjectRequest); + Thread.sleep(1000); // Async call, need to wait. Is there a better way to test this? + + // Verify failure to start event occurred + verify(mockEventService, atLeastOnce()).triggerEvent(jupyterServerEventCaptor.capture()); + JupyterServerEventI capturedEvent = jupyterServerEventCaptor.getValue(); + assertEquals(JupyterServerEventI.Status.Failed, capturedEvent.getStatus()); + assertEquals(JupyterServerEventI.Operation.Start, capturedEvent.getOperation()); + + // Verify user options are not saved + verify(mockUserOptionsEntityService, never()).createOrUpdate(any()); + + // Verify no attempts to start a server + verify(mockJupyterHubClient, never()).startServer(any(), any(), any()); + } + @Test(timeout = 4000) public void testStartServer_Timeout() throws Exception { // Grant permissions @@ -439,7 +466,7 @@ public void testStartServer_Timeout() throws Exception { // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), eq(projectId), eq(projectId), eq(computeEnvironmentConfigId), - eq(hardwareConfigId), eq(eventTrackingId)); + eq(hardwareConfigId), isNull(), eq(eventTrackingId)); // Verify JupyterHub start server request sent verify(mockJupyterHubClient, times(1)).startServer(eq(username), eq(""), any(UserOptions.class)); @@ -474,7 +501,7 @@ public void testStartServer_Success() throws Exception { // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), eq(projectId), eq(projectId), eq(computeEnvironmentConfigId), - eq(hardwareConfigId), eq(eventTrackingId)); + eq(hardwareConfigId), isNull(), eq(eventTrackingId)); // Verify JupyterHub start server request sent verify(mockJupyterHubClient, times(1)).startServer(eq(username), eq(""), any(UserOptions.class)); @@ -511,7 +538,7 @@ public void testStartServer_CreateUser_Success() throws Exception { // Verify user options are stored verify(mockUserOptionsService, times(1)).storeUserOptions(eq(user), eq(""), eq(XnatProjectdata.SCHEMA_ELEMENT_NAME), eq(projectId), eq(projectId), eq(computeEnvironmentConfigId), - eq(hardwareConfigId), eq(eventTrackingId)); + eq(hardwareConfigId), isNull(), eq(eventTrackingId)); // Verify JupyterHub start server request sent verify(mockJupyterHubClient, times(1)).startServer(eq(username), eq(""), any(UserOptions.class)); diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardFrameworkEntityServiceTest.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardFrameworkEntityServiceTest.java new file mode 100644 index 0000000..756f9cb --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/services/impl/HibernateDashboardFrameworkEntityServiceTest.java @@ -0,0 +1,112 @@ +package org.nrg.xnatx.plugins.jupyterhub.services.impl; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.nrg.framework.exceptions.NotFoundException; +import org.nrg.xnatx.plugins.jupyterhub.config.HibernateDashboardFrameworkEntityServiceTestConfig; +import org.nrg.xnatx.plugins.jupyterhub.entities.DashboardFrameworkEntity; +import org.nrg.xnatx.plugins.jupyterhub.models.DashboardFramework; +import org.nrg.xnatx.plugins.jupyterhub.repositories.DashboardFrameworkEntityDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.transaction.Transactional; + +import java.util.Optional; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; +import static org.nrg.xnatx.plugins.jupyterhub.utils.TestingUtils.commitTransaction; + +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +@ContextConfiguration(classes = HibernateDashboardFrameworkEntityServiceTestConfig.class) +public class HibernateDashboardFrameworkEntityServiceTest { + + @Autowired private HibernateDashboardFrameworkEntityService entityService; + @Qualifier("dashboardFrameworkEntityDaoImpl") @Autowired private DashboardFrameworkEntityDao dao; + + private DashboardFramework panel; + private DashboardFramework streamlit; + + @Before + public void setup() { + entityService.setDao(dao); + + panel = DashboardFramework.builder() + .name("Panel") + .commandTemplate("jhsingle-native-proxy --destport {port} --authtype none --user {username} --group {group} --debug") + .build(); + streamlit = DashboardFramework.builder() + .name("Streamlit") + .commandTemplate("jhsingle-native-proxy --destport {port} --authtype none --user {username} --group {group} --debug") + .build(); + } + + @After + public void after() { + Mockito.reset(); + } + + @Test + public void test() { + assertNotNull(entityService); + assertNotNull(dao); + } + + @Test + @DirtiesContext + public void testCreate() throws NotFoundException { + // Setup + DashboardFrameworkEntity panelEntity = DashboardFrameworkEntity.fromPojo(panel); + DashboardFrameworkEntity streamlitEntity = DashboardFrameworkEntity.fromPojo(streamlit); + + // Execute + DashboardFrameworkEntity createdPanel = entityService.create(panelEntity); + DashboardFrameworkEntity createdStreamlit = entityService.create(streamlitEntity); + + commitTransaction(); + + DashboardFrameworkEntity retrievedPanel = entityService.get(createdPanel.getId()); + DashboardFrameworkEntity retrievedStreamlit = entityService.get(createdStreamlit.getId()); + + // Verify + assertEquals(2, entityService.getAll().size()); + assertThat(retrievedPanel.toPojo(), is(createdPanel.toPojo())); + assertThat(retrievedStreamlit.toPojo(), is(createdStreamlit.toPojo())); + } + + @Test + @DirtiesContext + public void testFindFrameworkByName() { + // Setup + DashboardFrameworkEntity panelEntity = DashboardFrameworkEntity.fromPojo(panel); + DashboardFrameworkEntity streamlitEntity = DashboardFrameworkEntity.fromPojo(streamlit); + + // Execute + DashboardFrameworkEntity createdPanel = entityService.create(panelEntity); + DashboardFrameworkEntity createdStreamlit = entityService.create(streamlitEntity); + + // Update pojo IDs + panel.setId(createdPanel.getId()); + streamlit.setId(createdStreamlit.getId()); + + commitTransaction(); + + Optional retrievedPanel = entityService.findFrameworkByName(panel.getName()); + Optional retrievedStreamlit = entityService.findFrameworkByName(streamlit.getName()); + + // Verify + assertTrue(retrievedPanel.isPresent()); + assertTrue(retrievedStreamlit.isPresent()); + assertThat(retrievedPanel.get().toPojo(), is(panel)); + assertThat(retrievedStreamlit.get().toPojo(), is(streamlit)); + } + +} \ No newline at end of file diff --git a/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/TestingUtils.java b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/TestingUtils.java new file mode 100644 index 0000000..5f072f9 --- /dev/null +++ b/src/test/java/org/nrg/xnatx/plugins/jupyterhub/utils/TestingUtils.java @@ -0,0 +1,15 @@ +package org.nrg.xnatx.plugins.jupyterhub.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.test.context.transaction.TestTransaction; + +@Slf4j +public class TestingUtils { + + public static void commitTransaction() { + TestTransaction.flagForCommit(); + TestTransaction.end(); + TestTransaction.start(); + } + +} \ No newline at end of file From 081c15d23a0f3a3baebeb1014b7159834ec76cc0 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 8 Jan 2024 10:43:13 -0600 Subject: [PATCH 43/49] JHP-74 and XNAT-7903: Add to changelog --- CHANGELOG.MD | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 8bc8950..ed7d330 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [JHP-73][]: Adds dashboards functionality to the JupyterHub plugin. May require browser cache to be cleared. +- [XNAT-7903][] and [JHP-74][]: Support mounting of shared data at the project level. ### Changed @@ -28,6 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 to their Jupyter notebook server. Addresses an issue encountered in multi-node XNAT deployments. The new preference will default to the site URL (which will keep the current behavior). + +[XNAT-7903]: https://radiologics.atlassian.net/browse/XNAT-7903 [JHP-68]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-68 [JHP-69]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-69 [JHP-73]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-73 +[JHP-74]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-74 From 6811766afbb4e2e0262e0ff044c0b1a7b800e9a0 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 8 Jan 2024 10:45:59 -0600 Subject: [PATCH 44/49] JHP-66 and XNAT-7867: Added to CHANGELOG.MD --- CHANGELOG.MD | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index ed7d330..b4c716a 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -14,7 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Requires XNAT 1.8.10 or higher. +- [JHP-66][] and [XNAT-7867][]: Moves the compute services code from the plugin to the XNAT core to allow other plugins + to use the compute services. Makes the plugin dependent on XNAT 1.8.10 or later. + ## [1.0.1] - 2023-10-12 @@ -29,8 +31,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 to their Jupyter notebook server. Addresses an issue encountered in multi-node XNAT deployments. The new preference will default to the site URL (which will keep the current behavior). - +[XNAT-7867]: https://radiologics.atlassian.net/browse/XNAT-7867 [XNAT-7903]: https://radiologics.atlassian.net/browse/XNAT-7903 +[JHP-66]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-66 [JHP-68]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-68 [JHP-69]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-69 [JHP-73]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-73 From 8eb939848e2873dd8721350a1809c0dd843bf1d8 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Mon, 8 Jan 2024 10:49:16 -0600 Subject: [PATCH 45/49] JHP-70: Added to CHANGELOG.MD --- CHANGELOG.MD | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index b4c716a..12d001f 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [JHP-73][]: Adds dashboards functionality to the JupyterHub plugin. May require browser cache to be cleared. - [XNAT-7903][] and [JHP-74][]: Support mounting of shared data at the project level. +- [JHP-70]: Adds capability to configure plugin preferences via environment variables at startup. ### Changed @@ -36,5 +37,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [JHP-66]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-66 [JHP-68]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-68 [JHP-69]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-69 +[JHP-70]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-70 [JHP-73]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-73 [JHP-74]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-74 From 3e3db82b9d3beaa6da3f3dc26324985e7cc5add3 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Tue, 6 Feb 2024 13:40:17 -0600 Subject: [PATCH 46/49] JHP-77: Add GitHub workflows to build the develop branch and to release v*.*.* tagged commits as a GitHub release. --- .github/workflows/develop.yml | 38 ++++++++++++++++++++++++ .github/workflows/release.yml | 40 ++++++++++++++++++++++++++ build.gradle | 54 +++++++---------------------------- 3 files changed, 88 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/develop.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml new file mode 100644 index 0000000..c809e57 --- /dev/null +++ b/.github/workflows/develop.yml @@ -0,0 +1,38 @@ +name: Develop + +on: + push: + branches: + - develop + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + java-version: '8' + distribution: 'zulu' + java-package: jdk + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + gradle-version: 6.7 + + - name: Build JAR with Gradle Wrapper + run: ./gradlew clean jar + + - name: Upload JAR + uses: actions/upload-artifact@v4 + with: + name: plugin-jar + path: build/libs/*.jar + retention-days: 14 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1607d79 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + +jobs: + + release: + + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + java-version: '8' + distribution: 'zulu' + java-package: jdk + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + gradle-version: 6.7 + + - name: Build JAR with Gradle Wrapper + run: ./gradlew clean jar + + - name: Publish Release + uses: softprops/action-gh-release@v1 + with: + files: build/libs/*.jar + prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') || contains(github.ref, '-alpha') }} + body: The plugin JAR is attached to this release. diff --git a/build.gradle b/build.gradle index 5bbc4d1..bdace61 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,6 @@ plugins { id "java" id "org.nrg.xnat.build.xnat-data-builder" version "1.8.8" id "io.freefair.lombok" version "6.0.0-m2" - id "com.palantir.git-version" version "0.12.1" id "jacoco" } @@ -57,54 +56,21 @@ test { useJUnit() } -// Pulls in the Jenkins BUILD_NUMBER environment variable if available. def buildDate = new Date() -def buildNumber = System.getenv().BUILD_NUMBER?.toInteger() ?: "Manual" -def isDirty, branchName, gitHash, gitHashFull, commitDistance, lastTag, isCleanTag -try { - def gitDetails = versionDetails() - isDirty = gitVersion().endsWith ".dirty" - branchName = gitDetails.branchName ?: "Unknown" - gitHash = gitDetails.gitHash - gitHashFull = gitDetails.gitHashFull - commitDistance = gitDetails.commitDistance - lastTag = gitDetails.lastTag - isCleanTag = gitDetails.isCleanTag -} catch (IllegalArgumentException ignored) { - logger.info "Got an error trying to read VCS metadata from git. It's possible this project is not under VCS control. Using placeholder values for manifest entries." - isDirty = true - branchName = "Unknown" - gitHash = "None" - gitHashFull = "None" - commitDistance = 0 - lastTag = "None" - isCleanTag = false -} - -logger.info "Build-Date: ${buildDate}" -logger.info "Build-Number: ${buildNumber}" -logger.info "Implementation-Version: ${version}" -logger.info "Implementation-Sha-Full: ${gitHashFull}" -logger.info "Implementation-Sha: ${gitHash}" -logger.info "Implementation-Commit: ${commitDistance}" -logger.info "Implementation-LastTag: ${lastTag}" -logger.info "Implementation-Branch: ${branchName}" -logger.info "Implementation-CleanTag: ${isCleanTag}" -logger.info "Implementation-Dirty: ${isDirty}" +// Pulls in GitHub Actions environment variables if available. +def github_actor = System.getenv().GITHUB_ACTOR ?: "Unknown" +def github_repo = System.getenv().GITHUB_REPOSITORY ?: "Unknown" +def github_sha = System.getenv().GITHUB_SHA ?: "Unknown" +def github_ref = System.getenv().GITHUB_REF ?: "Unknown" ext.gitManifest = manifest { - attributes "Application-Name": "XNAT-JUPYTERHUB-PLUGIN", + attributes "Application-Name": "PIXI-PLUGIN", "Build-Date": buildDate, - "Build-Number": buildNumber, - "Implementation-Version": project.version, - "Implementation-Sha": gitHash, - "Implementation-Sha-Full": gitHashFull, - "Implementation-Commit": commitDistance, - "Implementation-LastTag": lastTag, - "Implementation-Branch": branchName, - "Implementation-CleanTag": isCleanTag, - "Implementation-Dirty": isDirty + "GITHUB-ACTOR": github_actor, + "GITHUB-REPO": github_repo, + "GITHUB-SHA": github_sha, + "GITHUB-REF": github_ref } jacocoTestReport { From 3cead218c13bbf8b7e3efce9e830d213979fdb5b Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Tue, 6 Feb 2024 13:46:55 -0600 Subject: [PATCH 47/49] JHP-77: Add to CHANGELOG.MD --- CHANGELOG.MD | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 12d001f..86614fb 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [JHP-66][] and [XNAT-7867][]: Moves the compute services code from the plugin to the XNAT core to allow other plugins to use the compute services. Makes the plugin dependent on XNAT 1.8.10 or later. +- [JHP-77][]: Add GitHub workflows to build the develop branch and publish releases. Move the build process to GitHub + Actions and remove the Jenkins build process. ## [1.0.1] - 2023-10-12 @@ -40,3 +42,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [JHP-70]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-70 [JHP-73]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-73 [JHP-74]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-74 +[JHP-77]: https://radiologics.atlassian.net/jira/software/c/projects/JHP/issues/JHP-77 From 07cf3d55dfe4c4ddae38bfe62f56b26733323931 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Tue, 6 Feb 2024 16:57:04 -0600 Subject: [PATCH 48/49] Bump version to 1.1.0-rc.snapshot --- CHANGELOG.MD | 2 +- build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 86614fb..1889703 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.1.0-rc.snapshot] - 2024-02-06 ### Added diff --git a/build.gradle b/build.gradle index bdace61..0af5aca 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "1.1.0-SNAPSHOT" +version "1.1.0-rc.snapshot" description "JupyterHub Plugin for XNAT" repositories { @@ -27,7 +27,7 @@ configurations { } dependencies { - xnatProvided platform("org.nrg:parent:1.8.10-SNAPSHOT") + xnatProvided platform("org.nrg:parent:1.8.10-RC-SNAPSHOT") xnatProvided "org.nrg:framework" xnatProvided "org.nrg.xnat:xnat-data-models" xnatProvided "org.nrg.xnat:web" From 92bea543ead1223588d3f1ade25a26dd97d189d7 Mon Sep 17 00:00:00 2001 From: Andy Lassiter Date: Wed, 7 Feb 2024 09:14:12 -0600 Subject: [PATCH 49/49] JHP-77: Make note of repo migration in CHANGELOG.MD --- CHANGELOG.MD | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 1889703..58b3851 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -18,7 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [JHP-66][] and [XNAT-7867][]: Moves the compute services code from the plugin to the XNAT core to allow other plugins to use the compute services. Makes the plugin dependent on XNAT 1.8.10 or later. - [JHP-77][]: Add GitHub workflows to build the develop branch and publish releases. Move the build process to GitHub - Actions and remove the Jenkins build process. + Actions and remove the Jenkins build process. Repository has migrated from + [Bitbucket](https://bitbucket.org/xnatx/xnat-jupyterhub-plugin/) to + [GitHub](https://github.com/NrgXnat/xnat-jupyterhub-plugin) ## [1.0.1] - 2023-10-12