diff --git a/containers/session-containers/skaha-desktop/Dockerfile.centos.xfce.vnc b/containers/session-containers/skaha-desktop/Dockerfile.centos.xfce.vnc index f476845f..a1557d0d 100755 --- a/containers/session-containers/skaha-desktop/Dockerfile.centos.xfce.vnc +++ b/containers/session-containers/skaha-desktop/Dockerfile.centos.xfce.vnc @@ -9,7 +9,8 @@ LABEL io.k8s.description="Headless VNC Container with Xfce window manager, firef io.openshift.non-scalable=true # BM: Install fonts so remote displays work -RUN yum update -y && yum install -y xorg-x11-fonts* gedit rsync gimp gimp-gmic +RUN yum update -y && yum install -y xorg-x11-fonts* gedit rsync gimp gimp-gmic gnome-terminal epel-release +RUN yum update -y && yum install -y jq ## Connection ports for controlling the UI: # VNC port:5901 diff --git a/containers/session-containers/skaha-desktop/VERSION b/containers/session-containers/skaha-desktop/VERSION index b106e4a6..7c237465 100644 --- a/containers/session-containers/skaha-desktop/VERSION +++ b/containers/session-containers/skaha-desktop/VERSION @@ -1,4 +1,4 @@ ## deployable containers have a semantic and build tag # semantic version tag: major.minor # build version tag: timestamp -TAGS="1.0.2 $(date -u +"%Y%m%dT%H%M%S")" +TAGS="1.1.0 $(date -u +"%Y%m%dT%H%M%S")" diff --git a/containers/session-containers/skaha-desktop/conf/xfce-applications.menu b/containers/session-containers/skaha-desktop/conf/xfce-applications.menu index b5162ea2..a1405b3d 100644 --- a/containers/session-containers/skaha-desktop/conf/xfce-applications.menu +++ b/containers/session-containers/skaha-desktop/conf/xfce-applications.menu @@ -163,15 +163,12 @@ Settings Screensaver + TerminalEmulator gpk-application.desktop gpk-update-viewer.desktop + xfce4-session-logout.desktop - - - xfce4-session-logout.desktop - - @@ -179,7 +176,12 @@ xfce-other.directory - + + + + + TerminalEmulator + diff --git a/deployment/k8s-config/kustomize/base/skaha-workload/config/build-menu.sh b/deployment/k8s-config/kustomize/base/skaha-workload/config/build-menu.sh index 5df3f015..57c8b749 100644 --- a/deployment/k8s-config/kustomize/base/skaha-workload/config/build-menu.sh +++ b/deployment/k8s-config/kustomize/base/skaha-workload/config/build-menu.sh @@ -41,6 +41,9 @@ init () { mkdir -p ${XFCE_DESKTOP_DIR_PARENT} ln -s ${DESKTOP_DIR} ${XFCE_DESKTOP_DIR} fi + + # sleep-forever.sh is used on desktop-app start up, refer to start-software-sh.template + cp /skaha-system/sleep-forever.sh ${EXECUTABLE_DIR}/. } build_resolution_items () { @@ -131,11 +134,15 @@ build_menu_item () { name=$2 category=$3 executable="${EXECUTABLE_DIR}/${name}.sh" + start_executable="${EXECUTABLE_DIR}/start-${name}.sh" desktop="${DESKTOP_DIR}/${name}.desktop" cp ${STARTUP_DIR}/software-sh.template $executable + cp ${STARTUP_DIR}/start-software-sh.template ${start_executable} cp ${STARTUP_DIR}/software-category.template $desktop sed -i -e "s#(IMAGE_ID)#${image_id}#g" $executable sed -i -e "s#(NAME)#${name}#g" $executable + sed -i -e "s#(IMAGE_ID)#${image_id}#g" ${start_executable} + sed -i -e "s#(NAME)#${name}#g" ${start_executable} sed -i -e "s#(NAME)#${name}#g" $desktop sed -i -e "s#(EXECUTABLE)#${EXECUTABLE_DIR}#g" $desktop sed -i -e "s#(CATEGORY)#${category}#g" $desktop diff --git a/deployment/k8s-config/kustomize/base/skaha-workload/config/sleep-forever.sh b/deployment/k8s-config/kustomize/base/skaha-workload/config/sleep-forever.sh new file mode 100755 index 00000000..5f13bbdf --- /dev/null +++ b/deployment/k8s-config/kustomize/base/skaha-workload/config/sleep-forever.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# sleep forever +while true; do + sleep 1000 +done diff --git a/deployment/k8s-config/kustomize/base/skaha-workload/config/templates/software-sh.template b/deployment/k8s-config/kustomize/base/skaha-workload/config/templates/software-sh.template index d1786634..0758abaf 100755 --- a/deployment/k8s-config/kustomize/base/skaha-workload/config/templates/software-sh.template +++ b/deployment/k8s-config/kustomize/base/skaha-workload/config/templates/software-sh.template @@ -1,3 +1,3 @@ #!/bin/bash -/opt/shibboleth/bin/curl -v -L -k -E ${HOME}/.ssl/cadcproxy.pem -d "image=(IMAGE_ID)" --data-urlencode "param=(NAME)" https://${skaha_hostname}/skaha/v0/session/${VNC_PW}/app +gnome-terminal -q --title="(NAME) launcher" -- ${HOME}/.local/skaha/bin/start-(NAME).sh diff --git a/deployment/k8s-config/kustomize/base/skaha-workload/config/templates/start-software-sh.template b/deployment/k8s-config/kustomize/base/skaha-workload/config/templates/start-software-sh.template new file mode 100644 index 00000000..2993f17e --- /dev/null +++ b/deployment/k8s-config/kustomize/base/skaha-workload/config/templates/start-software-sh.template @@ -0,0 +1,142 @@ +#!/bin/bash + +handle_error() { + echo "$1" + echo "Please enter Ctl+C when you are ready to exit the xterm." + ${HOME}/.local/skaha/bin/sleep-forever.sh & + wait + exit 1 +} + +get_resource_options() { + resources=`curl -s -L -k -E ${HOME}/.ssl/cadcproxy.pem https://${skaha_hostname}/skaha/v0/context` + core_default=`echo $resources | jq .defaultCores` + core_options=`echo $resources | jq .availableCores[] | tr '\n' ' '` + ram_default=`echo $resources | jq .defaultRAM` + ram_options=`echo $resources | jq .availableRAM[] | tr '\n' ' '` +} + +get_cores() { + local core_list=( ${core_options} ) + for v in "${core_list[@]}"; do + local core_map[$v]=1 + done + + cores=${core_default} + local c=0 + read -p "Please enter the number of cores (${core_options}) [${core_default}]: " input_cores + while (( c < 3 )); do + if [[ -z "${input_cores}" ]]; then + cores=${core_default} + echo "${cores}" + break + elif [[ -n "${core_map[${input_cores}]}" ]]; then + cores=${input_cores} + break + else + read -p "Please enter the number of cores (${core_options}) [${core_default}]: " input_cores + c=$(( c + 1 )) + fi + done + + if (( c > 2 )); then + handle_error "Failed to get the number of cores from user." + fi +} + +get_ram() { + local ram_list=( ${ram_options} ) + for v in "${ram_list[@]}"; do + local ram_map[$v]=1 + done + + ram=${ram_default} + local c=0 + read -p "Please enter the amount of memory in GB (${ram_options}) [${ram_default}]: " input_ram + while (( c < 3 )); do + if [[ -z "${input_ram}" ]]; then + ram=${ram_default} + echo "${ram}" + break + elif [[ -n "${ram_map[${input_ram}]}" ]]; then + ram=${input_ram} + break + else + read -p "Please enter the amount of memory in GB (${ram_options}) [${ram_default}]: " input_ram + c=$(( c + 1 )) + fi + done + + if (( c > 2 )); then + handle_error "Failed to get the amount of ram from user." + fi +} + +prompt_user() { + while true; do + read -p "Do you want to specify resources for (NAME)? (y/n) [n]" yn + if [[ -z "${yn}" || ${yn} == "n" || ${yn} == "N" ]]; then + echo "Launching (NAME)..." + app_id=`curl -s -L -k -E ${HOME}/.ssl/cadcproxy.pem -d "image=(IMAGE_ID)" --data-urlencode "param=(NAME)" https://${skaha_hostname}/skaha/v0/session/${VNC_PW}/app` + break + elif [[ ${yn} == "y" || ${yn} == "Y" ]]; then + get_resource_options || handle_error "Error obtaining resource defaults or options." + get_cores || handle_error "Error obtaining the number of cores to allocate." + get_ram || handle_error "Error obtaining the amount of ram to allocate." + echo "Launching (NAME)..." + app_id=`curl -s -L -k -E ${HOME}/.ssl/cadcproxy.pem -d "cores=${cores}" -d "ram=$ram" -d "image=(IMAGE_ID)" --data-urlencode "param=(NAME)" https://${skaha_hostname}/skaha/v0/session/${VNC_PW}/app` + break + else + echo invalid response + fi + done +} + +launch_app() { + get_resource_options || handle_error "Error obtaining resource defaults or options." + prompt_user || handle_error "Error prompting user inputs." +} + +get_status() { + curl_out="" + status="" + local n=0 + sleep 1 + curl_out=`curl -s -L -k -E ${HOME}/.ssl/cadcproxy.pem https://${skaha_hostname}/skaha/v0/session/${VNC_PW}/app/$1` + while [[ ${curl_out} != *"status"* ]]; do + n=$(( n + 1 )) + if test $n -eq 10 ; then + echo "Failed to get status, ${curl_out}, retrying..." + n=0 + fi + sleep 1 + curl_out=`curl -s -L -k -E ${HOME}/.ssl/cadcproxy.pem https://${skaha_hostname}/skaha/v0/session/${VNC_PW}/app/$1` + done +} + +check_status() { + get_status $1 + status=`echo ${curl_out} | jq .status` + echo "status: ${status}" + local count=0 + while [[ ${status} == *"Pending"* ]] + do + count=$(( $count + 1 )) + get_status $1 + status=`echo ${curl_out} | jq .status` + if test $count -eq 10 ; then + echo "status: ${status}" + count=0 + fi + done + + if [[ ${status} == *"Running"* ]]; then + echo "Successfully launched app." + sleep 1 + else + handle_error "Failed to launch app, status is ${status}." + fi +} + +launch_app +check_status ${app_id} diff --git a/deployment/k8s-config/kustomize/base/skaha-workload/kustomization.yaml b/deployment/k8s-config/kustomize/base/skaha-workload/kustomization.yaml index 23e8b063..bebcc271 100644 --- a/deployment/k8s-config/kustomize/base/skaha-workload/kustomization.yaml +++ b/deployment/k8s-config/kustomize/base/skaha-workload/kustomization.yaml @@ -10,12 +10,14 @@ configMapGenerator: files: - config/start-desktop.sh - config/build-menu.sh + - config/sleep-forever.sh - name: templates files: - config/templates/skaha-resolutions.properties - config/templates/xfce-directory.template - config/templates/xfce-applications-menu-item.template - config/templates/software-sh.template + - config/templates/start-software-sh.template - config/templates/software-category.template - config/templates/resolution-sh.template - config/templates/resolution-desktop.template diff --git a/deployment/k8s-config/kustomize/base/skaha/config/catalina.properties b/deployment/k8s-config/kustomize/base/skaha/config/catalina.properties index 23c986e0..d83b1d40 100644 --- a/deployment/k8s-config/kustomize/base/skaha/config/catalina.properties +++ b/deployment/k8s-config/kustomize/base/skaha/config/catalina.properties @@ -5,4 +5,3 @@ ca.nrc.cadc.auth.PrincipalExtractor.enableClientCertHeader=true ca.nrc.cadc.util.Log4jInit.messageOnly=true # (default: ca.nrc.cadc.auth.NoOpIdentityManager) ca.nrc.cadc.auth.IdentityManager=ca.nrc.cadc.auth.ACIdentityManager - diff --git a/deployment/k8s-config/kustomize/base/skaha/config/k8s-resources.properties b/deployment/k8s-config/kustomize/base/skaha/config/k8s-resources.properties index b3815563..4d290386 100644 --- a/deployment/k8s-config/kustomize/base/skaha/config/k8s-resources.properties +++ b/deployment/k8s-config/kustomize/base/skaha/config/k8s-resources.properties @@ -1,13 +1,25 @@ # Defines the resources available to science containers ### -# Default number of cores when not specified +# Default number of requested cores when not specified +cores-default-request = 1 + +# Default maximum number of cores when not specified +cores-default-limit = 16 + +# Default number of cores (request=limit) when not specified cores-default = 2 # Default cores for headless jobs cores-default-headless = 1 -# Default memory (RAM) in GB when not specified +# Default requested memory (RAM) in GB when not specified +mem-gb-default-request = 4 + +# Default maximum memory (RAM) in GB when not specified +mem-gb-default-limit = 192 + +# Default memory (RAM) in GB (request=limit) when not specified mem-gb-default = 16 # Default RAM for headless jobs diff --git a/deployment/k8s-config/kustomize/base/skaha/config/launch-desktop-app.yaml b/deployment/k8s-config/kustomize/base/skaha/config/launch-desktop-app.yaml index 0c2ec565..652c8e07 100644 --- a/deployment/k8s-config/kustomize/base/skaha/config/launch-desktop-app.yaml +++ b/deployment/k8s-config/kustomize/base/skaha/config/launch-desktop-app.yaml @@ -4,6 +4,7 @@ kind: Job metadata: labels: canfar-net-sessionID: "${skaha.sessionid}" + canfar-net-appID: "${software.appid}" canfar-net-sessionType: "${skaha.sessiontype}" canfar-net-userid: "${skaha.userid}" name: "${software.jobname}" @@ -14,6 +15,7 @@ spec: metadata: labels: canfar-net-sessionID: "${skaha.sessionid}" + canfar-net-appID: "${software.appid}" canfar-net-sessionType: "${skaha.sessiontype}" canfar-net-userid: "${skaha.userid}" job-name: "${software.jobname}" @@ -51,12 +53,12 @@ spec: imagePullPolicy: Always resources: requests: - memory: "4Gi" - cpu: "1" + memory: "${software.requests.ram}" + cpu: "${software.requests.cores}" ephemeral-storage: "20Gi" limits: - memory: "192Gi" - cpu: "16" + memory: "${software.limits.ram}" + cpu: "${software.limits.cores}" ephemeral-storage: "200Gi" ports: - containerPort: 6000 diff --git a/deployment/k8s-config/kustomize/base/skaha/skaha-tomcat-deployment.yaml b/deployment/k8s-config/kustomize/base/skaha/skaha-tomcat-deployment.yaml index 9089f0ae..d4f46b19 100644 --- a/deployment/k8s-config/kustomize/base/skaha/skaha-tomcat-deployment.yaml +++ b/deployment/k8s-config/kustomize/base/skaha/skaha-tomcat-deployment.yaml @@ -42,7 +42,7 @@ spec: value: "ivo://cadc.nrc.ca/gms?skaha-users" - name: skaha.adminsgroup value: "ivo://cadc.nrc.ca/gms?skaha-admins" - image: images.canfar.net/skaha-system/skaha:0.11.1 + image: images.canfar.net/skaha-system/skaha:0.12.0 imagePullPolicy: Always #imagePullPolicy: IfNotPresent name: skaha-tomcat diff --git a/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/config/k8s-resources.properties b/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/config/k8s-resources.properties index a88c9827..e63b4f31 100644 --- a/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/config/k8s-resources.properties +++ b/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/config/k8s-resources.properties @@ -1,13 +1,25 @@ # Defines the resources available to science containers ### -# Default number of cores when not specified +# Default number of requested cores when not specified +cores-default-request = 1 + +# Default maximum number of cores when not specified +cores-default-limit = 8 + +# Default number of cores (request=limit) when not specified cores-default = 2 # Default cores for headless jobs cores-default-headless = 1 -# Default memory (RAM) in GB when not specified +# Default requested memory (RAM) in GB when not specified +mem-gb-default-request = 4 + +# Default maximum memory (RAM) in GB when not specified +mem-gb-default-limit = 192 + +# Default memory (RAM) in GB (request=limit) when not specified mem-gb-default = 16 # Default RAM for headless jobs diff --git a/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/config/launch-desktop-app.yaml b/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/config/launch-desktop-app.yaml index dfdbbc9b..65c1ac7a 100644 --- a/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/config/launch-desktop-app.yaml +++ b/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/config/launch-desktop-app.yaml @@ -4,6 +4,7 @@ kind: Job metadata: labels: canfar-net-sessionID: "${skaha.sessionid}" + canfar-net-appID: "${software.appid}" canfar-net-sessionType: "${skaha.sessiontype}" canfar-net-userid: "${skaha.userid}" name: "${software.jobname}" @@ -14,6 +15,7 @@ spec: metadata: labels: canfar-net-sessionID: "${skaha.sessionid}" + canfar-net-appID: "${software.appid}" canfar-net-sessionType: "${skaha.sessiontype}" canfar-net-userid: "${skaha.userid}" job-name: "${software.jobname}" @@ -51,12 +53,12 @@ spec: imagePullPolicy: Always resources: requests: - memory: "4Gi" - cpu: "1" + memory: "${software.requests.ram}" + cpu: "${software.requests.cores}" ephemeral-storage: "20Gi" limits: - memory: "192Gi" - cpu: "8" + memory: "${software.limits.ram}" + cpu: "${software.limits.cores}" ephemeral-storage: "200Gi" ports: - containerPort: 6000 diff --git a/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/skaha-tomcat-deployment.yaml b/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/skaha-tomcat-deployment.yaml index 37b33a2a..c873d548 100644 --- a/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/skaha-tomcat-deployment.yaml +++ b/deployment/k8s-config/kustomize/overlays/keel-dev/skaha/skaha-tomcat-deployment.yaml @@ -21,7 +21,7 @@ spec: value: "345600" - name: skaha.defaultquotagb value: "10" - image: images-rc.canfar.net/skaha-system/skaha:0.11.1 + image: images-rc.canfar.net/skaha-system/skaha:0.12.0 resources: requests: memory: "4Gi" diff --git a/skaha/VERSION b/skaha/VERSION index 3d325bf3..55efecde 100644 --- a/skaha/VERSION +++ b/skaha/VERSION @@ -1,4 +1,4 @@ ## deployable containers have a semantic and build tag # semantic version tag: major.minor # build version tag: timestamp -TAGS="0.11.1 $(date -u +"%Y%m%dT%H%M%S")" +TAGS="0.12.0 $(date -u +"%Y%m%dT%H%M%S")" diff --git a/skaha/src/intTest/java/org/opencadc/skaha/DesktopAppLifecycleTest.java b/skaha/src/intTest/java/org/opencadc/skaha/DesktopAppLifecycleTest.java new file mode 100644 index 00000000..99f0c30d --- /dev/null +++ b/skaha/src/intTest/java/org/opencadc/skaha/DesktopAppLifecycleTest.java @@ -0,0 +1,397 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2023. (c) 2023. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la “GNU Affero General Public + * License as published by the License” telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l’espoir qu’il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n’est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +package org.opencadc.skaha; + +import ca.nrc.cadc.auth.AuthMethod; +import ca.nrc.cadc.auth.SSLUtil; +import ca.nrc.cadc.net.HttpDelete; +import ca.nrc.cadc.net.HttpGet; +import ca.nrc.cadc.net.HttpPost; +import ca.nrc.cadc.reg.Standards; +import ca.nrc.cadc.reg.client.RegistryClient; +import ca.nrc.cadc.util.FileUtil; +import ca.nrc.cadc.util.Log4jInit; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import javax.security.auth.Subject; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.junit.Assert; +import org.junit.Test; +import org.opencadc.skaha.session.Session; +import org.opencadc.skaha.session.SessionAction; + +/** + * @author majorb + * + */ +public class DesktopAppLifecycleTest { + + private static final Logger log = Logger.getLogger(DesktopAppLifecycleTest.class); + public static final URI SKAHA_SERVICE_ID = URI.create("ivo://cadc.nrc.ca/skaha"); + public static final String PROC_SESSION_STDID = "vos://cadc.nrc.ca~vospace/CADC/std/Proc#sessions-1.0"; + public static final String DESKTOP_IMAGE = "images-rc.canfar.net/skaha/desktop:1.0.2"; + public static final String TERMINAL_IMAGE = "images-rc.canfar.net/skaha/terminal:1.1.2"; + + static { + Log4jInit.setLevel("org.opencadc.skaha", Level.INFO); + } + + protected URL sessionURL; + protected URL desktopAppURL; + protected Subject userSubject; + + public DesktopAppLifecycleTest() { + try { + RegistryClient regClient = new RegistryClient(); + sessionURL = regClient.getServiceURL(SKAHA_SERVICE_ID, Standards.PROC_SESSIONS_10, AuthMethod.CERT); + sessionURL = new URL(sessionURL.toString() + "/session"); + log.info("sessions URL: " + sessionURL); + + File cert = FileUtil.getFileFromResource("skaha-test.pem", DesktopAppLifecycleTest.class); + userSubject = SSLUtil.createSubject(cert); + log.debug("userSubject: " + userSubject); + } catch (Exception e) { + log.error("init exception", e); + throw new RuntimeException("init exception", e); + } + } + + @Test + public void testCreateDeleteDesktopApp() { + try { + + Subject.doAs(userSubject, new PrivilegedExceptionAction() { + + public Object run() throws Exception { + + // ensure that there is no active session + initialize(); + + // create desktop session + String sessionID = createSession(DESKTOP_IMAGE); + desktopAppURL = new URL(sessionURL.toString() + "/" + sessionID + "/app"); + log.info("desktop-app URL: " + desktopAppURL); + + // until issue 4 (https://github.com/opencadc/skaha/issues/4) has been + // addressed, just wait for a bit. + TimeUnit.SECONDS.sleep(10); + + // verify desktop session + verifyOneSession(SessionAction.SESSION_TYPE_DESKTOP, "#1"); + + // create a terminal desktop-app + String appID = createDesktopApp(TERMINAL_IMAGE); + + TimeUnit.SECONDS.sleep(10); + + // verify desktop session and desktop-app + int sessionCount = 0; + int appCount = 0; + List sessions = getSessions(); + String desktopSessionID = null; + String desktopAppID = null; + for (Session s : sessions) { + Assert.assertNotNull("session type", s.getType()); + Assert.assertNotNull("session has no status", s.getStatus()); + if (s.getStatus().equals("Running")) { + if (s.getType().equals(SessionAction.SESSION_TYPE_DESKTOP)) { + sessionCount++; + desktopSessionID = s.getId(); + Assert.assertEquals("session name", "inttest", s.getName()); + } else if (s.getType().equals(SessionAction.TYPE_DESKTOP_APP)) { + appCount++; + desktopAppID = s.getAppId(); + Assert.assertNotNull("app id", s.getAppId()); + } else { + throw new AssertionError("invalid session type: " + s.getType()); + } + + Assert.assertNotNull("session id", s.getId()); + Assert.assertNotNull("connect URL", s.getConnectURL()); + Assert.assertNotNull("up since", s.getStartTime()); + } + } + Assert.assertTrue("one session", sessionCount == 1); + Assert.assertTrue("one desktop-app", appCount == 1); + Assert.assertNotNull("no desktop session", desktopSessionID); + Assert.assertNotNull("no desktop app", desktopAppID); + Assert.assertEquals(appID, desktopAppID); + + // get desktop-app + List desktopApps = getAllDesktopApp(); + Assert.assertFalse("no desktop-app", desktopApps.isEmpty()); + Assert.assertTrue("more than one desktop-app", desktopApps.size() == 1); + + // delete desktop-app + deleteDesktopApp(desktopAppURL, desktopAppID); + TimeUnit.SECONDS.sleep(10); + desktopApps = getAllDesktopApp(); + Assert.assertTrue("should have no active desktop-app", desktopApps.isEmpty()); + + // create desktop-app specifying resources + String cores = "2"; + String ram = "2"; + desktopAppID = createDesktopApp(TERMINAL_IMAGE, cores, ram); + TimeUnit.SECONDS.sleep(10); + desktopApps = getAllDesktopApp(); + Assert.assertFalse("no desktop-app", desktopApps.isEmpty()); + Assert.assertTrue("more than one desktop-app", desktopApps.size() == 1); + Session desktopApp = desktopApps.get(0); + Assert.assertEquals("wrong number of cores", desktopApp.getRequestedCPUCores(), cores); + Assert.assertEquals("wrong amount of ram", desktopApp.getRequestedRAM(), ram + "G"); + + // delete desktop-app + deleteDesktopApp(desktopAppURL, desktopAppID); + TimeUnit.SECONDS.sleep(10); + desktopApps = getAllDesktopApp(); + Assert.assertTrue("should have no active desktop-app", desktopApps.isEmpty()); + + // verify remaining desktop session + verifyOneSession(SessionAction.SESSION_TYPE_DESKTOP, "#2"); + + // delete desktop session + deleteSession(sessionURL, desktopSessionID); + + TimeUnit.SECONDS.sleep(10); + + // verify that there is no session left + sessionCount = 0; + sessions = getSessions(); + for (Session s : sessions) { + Assert.assertNotNull("session ID", s.getId()); + if (s.getId().equals(desktopSessionID)) { + sessionCount++; + } + } + Assert.assertTrue("zero sessions #2", sessionCount == 0); + + + return null; + } + }); + + } catch (Exception t) { + log.error("unexpected throwable", t); + Assert.fail("unexpected throwable: " + t); + } + + } + + private void initialize() throws MalformedURLException, InterruptedException { + List sessions = getSessions(); + boolean wait = false; + for (Session session : sessions) { + if (session.getType().equals(SessionAction.TYPE_DESKTOP_APP)) { + // delete desktop-app + wait = true; + String sessionID = session.getId(); + desktopAppURL = new URL(sessionURL.toString() + "/" + sessionID + "/app"); + deleteDesktopApp(sessionURL, session.getId()); + } else { + // delete session + wait = true; + deleteSession(sessionURL, session.getId()); + } + } + + if (wait) { + TimeUnit.SECONDS.sleep(10); + } + + int count = 0; + sessions = getSessions(); + for (Session s : sessions) { + count++; + } + Assert.assertTrue("zero sessions #1", count == 0); + } + + private String createSession(String image) throws IOException { + Map params = new HashMap(); + params.put("name", "inttest"); + params.put("image", image); + HttpPost post = new HttpPost(sessionURL, params, false); + post.run(); + String sessionID = post.getResponseBody().trim(); + Assert.assertNull("create session error", post.getThrowable()); + return sessionID; + } + + private String createDesktopApp(String image, Map params) throws IOException { + HttpPost post = new HttpPost(desktopAppURL, params, false); + post.run(); + String appID = post.getResponseBody().trim(); + Assert.assertNull("create desktop-app error", post.getThrowable()); + return appID; + } + + private String createDesktopApp(String image) throws IOException { + Map params = new HashMap(); + params.put("image", image); + return createDesktopApp(image, params); + } + + private String createDesktopApp(String image, String cores, String ram) throws IOException { + Map params = new HashMap(); + params.put("cores", cores); + params.put("ram", ram); + params.put("image", image); + return createDesktopApp(image, params); + } + + private void verifyOneSession(String expectedSessionType, String sessionNumber) { + int count = 0; + List sessions = getSessions(); + for (Session session : sessions) { + Assert.assertNotNull("no session type", session.getType()); + if (session.getType().equals(expectedSessionType)) { + Assert.assertNotNull("no session ID", session.getId()); + if (session.getStatus().equals("Running")) { + count++; + Assert.assertEquals("session name", "inttest", session.getName()); + Assert.assertNotNull("connect URL", session.getConnectURL()); + Assert.assertNotNull("up since", session.getStartTime()); + } + } + + } + Assert.assertTrue("one session " + sessionNumber, count == 1); + } + + private void deleteSession(URL sessionURL, String sessionID) throws MalformedURLException { + HttpDelete delete = new HttpDelete(new URL(sessionURL.toString() + "/" + sessionID), true); + delete.run(); + Assert.assertNull("delete session error", delete.getThrowable()); + } + + private void deleteDesktopApp(URL desktopAppURL, String appID) throws MalformedURLException { + HttpDelete delete = new HttpDelete(new URL(desktopAppURL.toString() + "/" + appID), true); + delete.run(); + Assert.assertNull("delete session error", delete.getThrowable()); + } + + private List getAllDesktopApp() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + HttpGet get = new HttpGet(desktopAppURL, out); + get.run(); + Assert.assertNull("get desktop app error", get.getThrowable()); + Assert.assertEquals("content-type", "application/json", get.getContentType()); + String json = out.toString(); + Type listType = new TypeToken>(){}.getType(); + Gson gson = new Gson(); + List sessions = gson.fromJson(json, listType); + List active = new ArrayList(); + for (Session s : sessions) { + if (!(s.getStatus().equals(Session.STATUS_TERMINATING) || s.getStatus().equals(Session.STATUS_SUCCEEDED))) { + active.add(s); + } + } + + return active; + + } + + private List getSessions() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + HttpGet get = new HttpGet(sessionURL, out); + get.run(); + Assert.assertNull("get sessions error", get.getThrowable()); + Assert.assertEquals("content-type", "application/json", get.getContentType()); + String json = out.toString(); + Type listType = new TypeToken>(){}.getType(); + Gson gson = new Gson(); + List sessions = gson.fromJson(json, listType); + List active = new ArrayList(); + for (Session s : sessions) { + if (!(s.getStatus().equals(Session.STATUS_TERMINATING) || s.getStatus().equals(Session.STATUS_SUCCEEDED))) { + active.add(s); + } + } + return active; + } + + +} diff --git a/skaha/src/intTest/java/org/opencadc/skaha/ExpiryTimeRenewalTest.java b/skaha/src/intTest/java/org/opencadc/skaha/ExpiryTimeRenewalTest.java index 3276a750..c1d908d8 100644 --- a/skaha/src/intTest/java/org/opencadc/skaha/ExpiryTimeRenewalTest.java +++ b/skaha/src/intTest/java/org/opencadc/skaha/ExpiryTimeRenewalTest.java @@ -111,11 +111,14 @@ public class ExpiryTimeRenewalTest { private static final Logger log = Logger.getLogger(ExpiryTimeRenewalTest.class); + private static final String HOST_PROPERTY = RegistryClient.class.getName() + ".host"; public static final URI SKAHA_SERVICE_ID = URI.create("ivo://cadc.nrc.ca/skaha"); public static final String PROC_SESSION_STDID = "vos://cadc.nrc.ca~vospace/CADC/std/Proc#sessions-1.0"; - public static final String DESKTOP_IMAGE = "images.canfar.net/skaha/desktop:1.0.2"; - public static final String TERMINAL_IMAGE = "images.canfar.net/skaha/terminal:1.1.2"; - public static final String CARTA_IMAGE = "images.canfar.net/skaha/carta:2.0"; + public static final String DESKTOP_IMAGE_SUFFIX = "/skaha/desktop:1.0.2"; + public static final String TERMINAL_IMAGE_SUFFIX = "/skaha/terminal:1.1.2"; + public static final String CARTA_IMAGE_SUFFIX = "/skaha/carta:3.0"; + public static final String PROD_IMAGE_HOST = "images.canfar.net"; + public static final String DEV_IMAGE_HOST = "images-rc.canfar.net"; public static final int SLEEP_TIME = 10; static { @@ -124,9 +127,21 @@ public class ExpiryTimeRenewalTest { protected URL sessionURL; protected Subject userSubject; + protected String imageHost = PROD_IMAGE_HOST; public ExpiryTimeRenewalTest() { try { + // determine image host + String hostP = System.getProperty(HOST_PROPERTY); + if (hostP == null || hostP.trim().length() == 0) { + throw new IllegalArgumentException("missing server host, check " + HOST_PROPERTY); + } else { + hostP = hostP.trim(); + if (hostP.startsWith("rc-")) { + imageHost = DEV_IMAGE_HOST; + } + } + RegistryClient regClient = new RegistryClient(); sessionURL = regClient.getServiceURL(SKAHA_SERVICE_ID, Standards.PROC_SESSIONS_10, AuthMethod.CERT); sessionURL = new URL(sessionURL.toString() + "/session"); @@ -152,7 +167,7 @@ public Object run() throws Exception { initialize(); // create carta session - createSession(CARTA_IMAGE); + createSession(imageHost + CARTA_IMAGE_SUFFIX); TimeUnit.SECONDS.sleep(SLEEP_TIME); @@ -238,7 +253,7 @@ public Object run() throws Exception { initialize(); // create headless session - createHeadlessSession(TERMINAL_IMAGE); + createHeadlessSession(imageHost + TERMINAL_IMAGE_SUFFIX); TimeUnit.SECONDS.sleep(SLEEP_TIME); @@ -312,7 +327,7 @@ public void testRenewDesktop() throws Exception { initialize(); // create desktop session - createSession(DESKTOP_IMAGE); + createSession(imageHost + DESKTOP_IMAGE_SUFFIX); TimeUnit.SECONDS.sleep(SLEEP_TIME); @@ -339,7 +354,7 @@ public void testRenewDesktop() throws Exception { // create desktop app URL desktopAppURL = new URL(sessionURL.toString() + "/" + desktopSessionID + "/app"); - createApp(TERMINAL_IMAGE, desktopAppURL); + createApp(imageHost + TERMINAL_IMAGE_SUFFIX, desktopAppURL); // get time to live (start time - stop time) before renewal count = 0; diff --git a/skaha/src/intTest/java/org/opencadc/skaha/LifecycleTest.java b/skaha/src/intTest/java/org/opencadc/skaha/SessionLifecycleTest.java similarity index 90% rename from skaha/src/intTest/java/org/opencadc/skaha/LifecycleTest.java rename to skaha/src/intTest/java/org/opencadc/skaha/SessionLifecycleTest.java index 94ed5a73..aaab1d83 100644 --- a/skaha/src/intTest/java/org/opencadc/skaha/LifecycleTest.java +++ b/skaha/src/intTest/java/org/opencadc/skaha/SessionLifecycleTest.java @@ -106,29 +106,44 @@ * @author majorb * */ -public class LifecycleTest { +public class SessionLifecycleTest { - private static final Logger log = Logger.getLogger(LifecycleTest.class); + private static final Logger log = Logger.getLogger(SessionLifecycleTest.class); + private static final String HOST_PROPERTY = RegistryClient.class.getName() + ".host"; public static final URI SKAHA_SERVICE_ID = URI.create("ivo://cadc.nrc.ca/skaha"); public static final String PROC_SESSION_STDID = "vos://cadc.nrc.ca~vospace/CADC/std/Proc#sessions-1.0"; - public static final String DESKTOP_IMAGE = "images.canfar.net/skaha/desktop:1.0.2"; - public static final String CARTA_IMAGE = "images.canfar.net/skaha/carta:2.0"; - + public static final String DESKTOP_IMAGE_SUFFIX = "/skaha/desktop:1.0.2"; + public static final String CARTA_IMAGE_SUFFIX = "/skaha/carta:3.0"; + public static final String PROD_IMAGE_HOST = "images.canfar.net"; + public static final String DEV_IMAGE_HOST = "images-rc.canfar.net"; + static { Log4jInit.setLevel("org.opencadc.skaha", Level.INFO); } protected URL sessionURL; protected Subject userSubject; - - public LifecycleTest() { + protected String imageHost = PROD_IMAGE_HOST; + + public SessionLifecycleTest() { try { + // determine image host + String hostP = System.getProperty(HOST_PROPERTY); + if (hostP == null || hostP.trim().length() == 0) { + throw new IllegalArgumentException("missing server host, check " + HOST_PROPERTY); + } else { + hostP = hostP.trim(); + if (hostP.startsWith("rc-")) { + imageHost = DEV_IMAGE_HOST; + } + } + RegistryClient regClient = new RegistryClient(); sessionURL = regClient.getServiceURL(SKAHA_SERVICE_ID, Standards.PROC_SESSIONS_10, AuthMethod.CERT); sessionURL = new URL(sessionURL.toString() + "/session"); log.info("sessions URL: " + sessionURL); - File cert = FileUtil.getFileFromResource("skaha-test.pem", LifecycleTest.class); + File cert = FileUtil.getFileFromResource("skaha-test.pem", SessionLifecycleTest.class); userSubject = SSLUtil.createSubject(cert); log.debug("userSubject: " + userSubject); } catch (Exception e) { @@ -145,7 +160,7 @@ public void testCreateDeleteSessions() throws Exception { initialize(); // create desktop session - createSession(DESKTOP_IMAGE); + createSession(imageHost + DESKTOP_IMAGE_SUFFIX); // until issue 4 (https://github.com/opencadc/skaha/issues/4) has been // addressed, just wait for a bit. @@ -155,7 +170,7 @@ public void testCreateDeleteSessions() throws Exception { verifyOneSession(SessionAction.SESSION_TYPE_DESKTOP, "#1"); // create carta session - createSession(CARTA_IMAGE); + createSession(imageHost + CARTA_IMAGE_SUFFIX); TimeUnit.SECONDS.sleep(10); @@ -280,7 +295,7 @@ private List getSessions() { List sessions = gson.fromJson(json, listType); List active = new ArrayList<>(); for (Session s : sessions) { - if (!s.getStatus().equals(Session.STATUS_TERMINATING)) { + if (!(s.getStatus().equals(Session.STATUS_TERMINATING) || s.getStatus().equals(Session.STATUS_SUCCEEDED))) { active.add(s); } } diff --git a/skaha/src/main/java/org/opencadc/skaha/context/ResourceContexts.java b/skaha/src/main/java/org/opencadc/skaha/context/ResourceContexts.java index e446fdba..a4fe3080 100644 --- a/skaha/src/main/java/org/opencadc/skaha/context/ResourceContexts.java +++ b/skaha/src/main/java/org/opencadc/skaha/context/ResourceContexts.java @@ -83,11 +83,15 @@ public class ResourceContexts { private static final Logger log = Logger.getLogger(ResourceContexts.class); + private Integer defaultRequestCores; + private Integer defaultLimitCores; private Integer defaultCores; private Integer defaultCoresHeadless; private List availableCores = new ArrayList(); // units in GB + private Integer defaultRequestRAM; + private Integer defaultLimitRAM; private Integer defaultRAM; private Integer defaultRAMHeadless; private List availableRAM = new ArrayList(); @@ -98,8 +102,12 @@ public ResourceContexts() { try { PropertiesReader reader = new PropertiesReader("k8s-resources.properties"); MultiValuedProperties mvp = reader.getAllProperties(); + defaultRequestCores = Integer.valueOf(mvp.getFirstPropertyValue("cores-default-request")); + defaultLimitCores = Integer.valueOf(mvp.getFirstPropertyValue("cores-default-limit")); defaultCores = Integer.valueOf(mvp.getFirstPropertyValue("cores-default")); defaultCoresHeadless = Integer.valueOf(mvp.getFirstPropertyValue("cores-default-headless")); + defaultRequestRAM = Integer.valueOf(mvp.getFirstPropertyValue("mem-gb-default-request")); + defaultLimitRAM = Integer.valueOf(mvp.getFirstPropertyValue("mem-gb-default-limit")); defaultRAM = Integer.valueOf(mvp.getFirstPropertyValue("mem-gb-default")); defaultRAMHeadless = Integer.valueOf(mvp.getFirstPropertyValue("mem-gb-default-headless")); String cOptions = mvp.getFirstPropertyValue("cores-options"); @@ -121,6 +129,14 @@ public ResourceContexts() { } } + public Integer getDefaultRequestCores() { + return defaultRequestCores; + } + + public Integer getDefaultLimitCores() { + return defaultLimitCores; + } + public Integer getDefaultCores(String sessionType) { if (SkahaAction.SESSION_TYPE_HEADLESS.equals(sessionType)) { return defaultCoresHeadless; @@ -132,6 +148,14 @@ public List getAvailableCores() { return availableCores; } + public Integer getDefaultRequestRAM() { + return defaultRequestRAM; + } + + public Integer getDefaultLimitRAM() { + return defaultLimitRAM; + } + public Integer getDefaultRAM(String sessionType) { if (SkahaAction.SESSION_TYPE_HEADLESS.equals(sessionType)) { return defaultRAMHeadless; diff --git a/skaha/src/main/java/org/opencadc/skaha/session/DeleteAction.java b/skaha/src/main/java/org/opencadc/skaha/session/DeleteAction.java index 4363c194..c2860dc4 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/DeleteAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/DeleteAction.java @@ -134,7 +134,7 @@ public void doAction() throws Exception { } if (requestType.equals(REQUEST_TYPE_APP)) { - throw new UnsupportedOperationException("App killing not supported."); + deleteSession(userID, TYPE_DESKTOP_APP, sessionID); } } @@ -143,18 +143,34 @@ public void deleteSession(String userID, String type, String sessionID) throws E log.debug("Stopping " + type + " session: " + sessionID); String k8sNamespace = K8SUtil.getWorkloadNamespace(); - String podName = K8SUtil.getJobName(sessionID, type, userID); - delete(k8sNamespace, "job", podName); - - if (!SESSION_TYPE_HEADLESS.equals(type)) { - String ingressName = K8SUtil.getIngressName(sessionID, type); - delete(k8sNamespace, "ingressroute", ingressName); + if (TYPE_DESKTOP_APP.equalsIgnoreCase(type)) { + // deleting a desktop-app + if (StringUtil.hasText(appID)) { + log.debug("appID " + appID); + String jobName = this.getAppJobName(sessionID, userID, appID); + if (StringUtil.hasText(jobName)) { + delete(k8sNamespace, "job", jobName); + } else { + log.warn("no job deleted, desktop-app job name not found for userID " + userID + ", sessionID " + sessionID + ", appID " + appID); + } + } else { + throw new IllegalArgumentException("Missing app ID"); + } + } else { + // deleting a session + String jobName = K8SUtil.getJobName(sessionID, type, userID); + delete(k8sNamespace, "job", jobName); - String serviceName = K8SUtil.getServiceName(sessionID, type); - delete(k8sNamespace, "service", serviceName); - - String middlewareName = K8SUtil.getMiddlewareName(sessionID, type); - delete(k8sNamespace, "middleware", middlewareName); + if (!SESSION_TYPE_HEADLESS.equals(type)) { + String ingressName = K8SUtil.getIngressName(sessionID, type); + delete(k8sNamespace, "ingressroute", ingressName); + + String serviceName = K8SUtil.getServiceName(sessionID, type); + delete(k8sNamespace, "service", serviceName); + + String middlewareName = K8SUtil.getMiddlewareName(sessionID, type); + delete(k8sNamespace, "middleware", middlewareName); + } } } diff --git a/skaha/src/main/java/org/opencadc/skaha/session/GetAction.java b/skaha/src/main/java/org/opencadc/skaha/session/GetAction.java index 4ef610c3..390ddb4f 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/GetAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/GetAction.java @@ -109,8 +109,8 @@ public GetAction() { @Override public void doAction() throws Exception { super.initRequest(); + String view = syncInput.getParameter("view"); if (requestType.equals(REQUEST_TYPE_SESSION)) { - String view = syncInput.getParameter("view"); if (sessionID == null) { if (SESSION_VIEW_STATS.equals(view)) { ResourceStats resourceStats = getResourceStats(); @@ -149,11 +149,20 @@ public void doAction() throws Exception { } return; } + if (requestType.equals(REQUEST_TYPE_APP)) { if (appID == null) { - throw new UnsupportedOperationException("App listing not supported."); + String statusFilter = syncInput.getParameter("status"); + boolean allUsers = SESSION_LIST_VIEW_ALL.equals(view); + String json = listSessions(SessionAction.TYPE_DESKTOP_APP, statusFilter, allUsers); + syncOutput.setHeader("Content-Type", "application/json"); + syncOutput.getOutputStream().write(json.getBytes()); + } else if (sessionID == null){ + throw new IllegalArgumentException("Missing session ID for desktop-app ID " + appID); } else { - throw new UnsupportedOperationException("App detail viewing not supported."); + String json = getSingleDesktopApp(sessionID, appID); + syncOutput.setHeader("Content-Type", "application/json"); + syncOutput.getOutputStream().write(json.getBytes()); } } } @@ -382,7 +391,13 @@ private Map getAvailableResources(String k8sNamespace) throws return nodeToResourcesMap; } - + + public String getSingleDesktopApp(String sessionID, String appID) throws Exception { + Session session = this.getDesktopApp(userID, sessionID, appID); + Gson gson = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); + return gson.toJson(session); + } + public String getSingleSession(String sessionID) throws Exception { Session session = this.getSession(userID, sessionID); Gson gson = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); diff --git a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java index 33ec8fc5..585fae13 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java @@ -128,6 +128,7 @@ public class PostAction extends SessionAction { public static final String SKAHA_SCHEDULEGPU = "skaha.schedulegpu"; public static final String SOFTWARE_JOBNAME = "software.jobname"; public static final String SOFTWARE_HOSTNAME = "software.hostname"; + public static final String SOFTWARE_APPID = "software.appid"; public static final String SOFTWARE_CONTAINERNAME = "software.containername"; public static final String SOFTWARE_CONTAINERPARAM = "software.containerparam"; public static final String SOFTWARE_TARGETIP = "software.targetip"; @@ -150,15 +151,31 @@ public PostAction() { public void doAction() throws Exception { super.initRequest(); + + String validatedType = null; + ResourceContexts rc = new ResourceContexts(); + String image = syncInput.getParameter("image"); + if (image == null) { + if (requestType.equals(REQUEST_TYPE_APP) || (requestType.equals(REQUEST_TYPE_SESSION) && sessionID == null)) { + throw new IllegalArgumentException("Missing parameter 'image'"); + } + } if (requestType.equals(REQUEST_TYPE_SESSION)) { if (sessionID == null) { - - String name = syncInput.getParameter("name"); - String image = syncInput.getParameter("image"); String type = syncInput.getParameter("type"); - String coresParam = syncInput.getParameter("cores"); - String ramParam = syncInput.getParameter("ram"); + validatedType = validateImage(image, type); + Integer cores = getCoresParam(); + if (cores == null) { + cores = rc.getDefaultCores(validatedType); + } + + Integer ram = getRamParam(); + if (ram == null ) { + ram = rc.getDefaultRAM(validatedType); + } + + String name = syncInput.getParameter("name"); String gpusParam = syncInput.getParameter("gpus"); String cmd = syncInput.getParameter("cmd"); String args = syncInput.getParameter("args"); @@ -166,11 +183,8 @@ public void doAction() throws Exception { if (name == null) { throw new IllegalArgumentException("Missing parameter 'name'"); } - if (image == null) { - throw new IllegalArgumentException("Missing parameter 'image'"); - } + validateName(name); - String validatedType = validateImage(image, type); // check for no existing session for this user // (rule: only 1 session of same type per user allowed) @@ -180,33 +194,7 @@ public void doAction() throws Exception { // (VNC passwords are only good up to 8 characters) sessionID = new RandomStringGenerator(8).getID(); - ResourceContexts rc = new ResourceContexts(); - Integer cores = rc.getDefaultCores(validatedType); - Integer ram = rc.getDefaultRAM(validatedType); - int gpus = 0; - - if (coresParam != null) { - try { - cores = Integer.valueOf(coresParam); - if (!rc.getAvailableCores().contains(cores)) { - throw new IllegalArgumentException("Unavailable option for 'cores': " + coresParam); - } - } catch (Exception e) { - throw new IllegalArgumentException("Invalid value for 'cores': " + coresParam); - } - } - - if (ramParam != null) { - try { - ram = Integer.valueOf(ramParam); - if (!rc.getAvailableRAM().contains(ram)) { - throw new IllegalArgumentException("Unavailable option for 'ram': " + ramParam); - } - } catch (Exception e) { - throw new IllegalArgumentException("Invalid value for 'ram': " + ramParam); - } - } - + Integer gpus = 0; if (gpusParam != null) { try { gpus = Integer.parseInt(gpusParam); @@ -223,7 +211,6 @@ public void doAction() throws Exception { // return the session id syncOutput.setHeader("Content-Type", "text/plain"); syncOutput.getOutputStream().write((sessionID + "\n").getBytes()); - } else { String action = syncInput.getParameter("action"); if (StringUtil.hasLength(action)) { @@ -245,20 +232,26 @@ public void doAction() throws Exception { } } return; - } - if (requestType.equals(REQUEST_TYPE_APP)) { + } else if (requestType.equals(REQUEST_TYPE_APP)) { if (appID == null) { // create an app - - // gather job parameters - String image = syncInput.getParameter("image"); - - if (image == null) { - throw new IllegalArgumentException("Missing parameter 'image'"); + Integer requestCores = getCoresParam(); + Integer limitCores = requestCores; + if (requestCores == null) { + requestCores = rc.getDefaultRequestCores(); + limitCores = rc.getDefaultLimitCores(); } - attachDesktopApp(image); + Integer requestRAM = getRamParam(); + Integer limitRAM = requestRAM; + if (requestRAM == null) { + requestRAM = rc.getDefaultRequestRAM(); + limitRAM = rc.getDefaultLimitRAM(); + } + attachDesktopApp(image, requestCores, limitCores, requestRAM, limitRAM); + syncOutput.setHeader("Content-Type", "text/plain"); + syncOutput.getOutputStream().write((appID + "\n").getBytes()); } else { throw new UnsupportedOperationException("Cannot modify an existing app."); } @@ -322,6 +315,43 @@ String getDefaultQuota() { return K8SUtil.getDefaultQuota(); } + private Integer getCoresParam() { + Integer cores = null; + String coresParam = syncInput.getParameter("cores"); + if (coresParam != null) { + try { + cores = Integer.valueOf(coresParam); + ResourceContexts rc = new ResourceContexts(); + if (!rc.getAvailableCores().contains(cores)) { + throw new IllegalArgumentException("Unavailable option for 'cores': " + coresParam); + } + } catch (Exception e) { + throw new IllegalArgumentException("Invalid value for 'cores': " + coresParam); + } + } + + return cores; + } + + private Integer getRamParam() { + Integer ram = null; + String ramParam = syncInput.getParameter("ram"); + if (ramParam != null) { + try { + ram = Integer.valueOf(ramParam); + ResourceContexts rc = new ResourceContexts(); + if (!rc.getAvailableRAM().contains(ram)) { + throw new IllegalArgumentException("Unavailable option for 'ram': " + ramParam); + } + } catch (Exception e) { + throw new IllegalArgumentException("Invalid value for 'ram': " + ramParam); + } + } + + return ram; + } + + private void renew(Map.Entry> entry) throws Exception { Long newExpiryTime = calculateExpiryTime(entry.getValue()); if (newExpiryTime > 0) { @@ -618,7 +648,7 @@ public void createSession(String sessionID, String type, String image, String na * @param image Container image name. * @throws Exception For any unexpected errors. */ - public void attachDesktopApp(String image) throws Exception { + public void attachDesktopApp(String image, Integer requestCores, Integer limitCores, Integer requestRAM, Integer limitRAM) throws Exception { String k8sNamespace = K8SUtil.getWorkloadNamespace(); @@ -627,9 +657,10 @@ public void attachDesktopApp(String image) throws Exception { "kubectl", "-n", k8sNamespace, "get", "pod", "--selector=canfar-net-sessionID=" + sessionID, "--no-headers=true", "-o", "custom-columns=" + - "IPADDR:.status.podIP," + - "DT:.metadata.deletionTimestamp," + - "TYPE:.metadata.labels.canfar-net-sessionType"}; + "IPADDR:.status.podIP," + + "DT:.metadata.deletionTimestamp," + + "TYPE:.metadata.labels.canfar-net-sessionType," + + "NAME:.metadata.name"}; String ipResult = execute(getIPCommand); log.debug("GET IP result: " + ipResult); @@ -674,9 +705,13 @@ public void attachDesktopApp(String image) throws Exception { // that it becomes the xterm title String param = name; log.debug("Using parameter: " + param); - - String uniqueID = new RandomStringGenerator(3).getID(); - String jobName = sessionID + "-" + uniqueID + "-" + userID.toLowerCase() + "-" + name.toLowerCase(); + log.debug("Using requests.cores: " + requestCores.toString()); + log.debug("Using limits.cores: " + limitCores.toString()); + log.debug("Using requests.ram: " + requestRAM.toString() + "Gi"); + log.debug("Using limits.ram: " + limitRAM.toString() + "Gi"); + + appID = new RandomStringGenerator(3).getID(); + String jobName = sessionID + "-" + appID + "-" + userID.toLowerCase() + "-" + name.toLowerCase(); String containerName = name.toLowerCase().replaceAll("\\.", "-"); // no dots in k8s names // trim job name if necessary if (jobName.length() > MAX_JOB_NAME_LENGTH) { @@ -697,7 +732,12 @@ public void attachDesktopApp(String image) throws Exception { launchString = setConfigValue(launchString, SOFTWARE_JOBNAME, jobName); launchString = setConfigValue(launchString, SOFTWARE_HOSTNAME, containerName); launchString = setConfigValue(launchString, SOFTWARE_CONTAINERNAME, containerName); + launchString = setConfigValue(launchString, SOFTWARE_APPID, appID); launchString = setConfigValue(launchString, SOFTWARE_CONTAINERPARAM, param); + launchString = setConfigValue(launchString, SOFTWARE_REQUESTS_CORES, requestCores.toString()); + launchString = setConfigValue(launchString, SOFTWARE_LIMITS_CORES, limitCores.toString()); + launchString = setConfigValue(launchString, SOFTWARE_REQUESTS_RAM, requestRAM.toString() + "Gi"); + launchString = setConfigValue(launchString, SOFTWARE_LIMITS_RAM, limitRAM.toString() + "Gi"); launchString = setConfigValue(launchString, SKAHA_USERID, userID); launchString = setConfigValue(launchString, SKAHA_SESSIONTYPE, SessionAction.TYPE_DESKTOP_APP); launchString = setConfigValue(launchString, SKAHA_SESSIONEXPIRY, K8SUtil.getSessionExpiry()); @@ -736,6 +776,7 @@ private String setConfigValue(String doc, String key, String value) { } private String getHarborSecret(String image) throws Exception { + // get the user's cli secret: // 1. get the idToken from /ac/authorize // 2. call harbor with idToken to get user info and secret diff --git a/skaha/src/main/java/org/opencadc/skaha/session/Session.java b/skaha/src/main/java/org/opencadc/skaha/session/Session.java index bdecf1ad..5cf73d04 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/Session.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/Session.java @@ -83,6 +83,7 @@ public class Session { private String id; private String userid; + private String appid; private String image; private String type; private String status; @@ -208,6 +209,14 @@ public void setExpiryTime(String timeInSeconds) { this.expiryTime = timeInSeconds; } + public String getAppId() { + return appid; + } + + public void setAppId(String appId) { + this.appid = appId; + } + @Override public boolean equals(Object o) { if (o instanceof Session) { diff --git a/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java b/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java index 8eb381a7..38547522 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java @@ -407,9 +407,24 @@ public void streamPodLogs(String forUserID, String sessionID, OutputStream out) execute(getLogsCmd.toArray(new String[0]), out); } + public Session getDesktopApp(String forUserID, String sessionID, String appID) throws Exception { + List sessions = getSessions(userID, sessionID); + if (sessions.size() > 0) { + for (Session session : sessions) { + // only include 'desktop-app' + if (SkahaAction.TYPE_DESKTOP_APP.equalsIgnoreCase(session.getType()) && + (sessionID.equals(session.getId())) && (appID.equals(session.getAppId()))) { + return session; + } + } + } + + throw new ResourceNotFoundException("desktop app with session " + sessionID + " and app ID " + appID + " was not found"); + } + public Session getSession(String forUserID, String sessionID) throws Exception { List sessions = getSessions(forUserID, sessionID); - if (sessions.size() >0) { + if (sessions.size() > 0) { for (Session session : sessions) { // exclude 'desktop-app' if (!SkahaAction.TYPE_DESKTOP_APP.equalsIgnoreCase(session.getType())) { @@ -664,7 +679,8 @@ private List getSessionsCMD(String k8sNamespace, String forUserID, Strin "STATUS:.status.phase," + "NAME:.metadata.labels.canfar-net-sessionName," + "STARTED:.status.startTime," + - "DELETION:.metadata.deletionTimestamp"; + "DELETION:.metadata.deletionTimestamp," + + "APPID:.metadata.labels.canfar-net-appID"; if (forUserID != null) { customColumns = customColumns + ",REQUESTEDRAM:.spec.containers[0].resources.requests.memory," + @@ -725,6 +741,40 @@ private List getSessionGPUUsageCMD(String k8sNamespace, String podName) return getSessionGPUCMD; } + protected String getAppJobName(String sessionID, String userID, String appID) throws IOException, InterruptedException { + String k8sNamespace = K8SUtil.getWorkloadNamespace(); + List getAppJobNameCMD = getAppJobNameCMD(k8sNamespace, userID, sessionID, appID); + return execute(getAppJobNameCMD.toArray(new String[0])); + } + + private List getAppJobNameCMD(String k8sNamespace, String userID, String sessionID, String appID) { + String labels = "canfar-net-sessionType=" + TYPE_DESKTOP_APP; + labels = labels + ",canfar-net-userid=" + userID; + if (sessionID != null) { + labels = labels + ",canfar-net-sessionID=" + sessionID; + } + if (appID != null) { + labels = labels + ",canfar-net-appID=" + appID; + } + + List getAppJobNameCMD = new ArrayList(); + getAppJobNameCMD.add("kubectl"); + getAppJobNameCMD.add("get"); + getAppJobNameCMD.add("--namespace"); + getAppJobNameCMD.add(k8sNamespace); + getAppJobNameCMD.add("job"); + getAppJobNameCMD.add("-l"); + getAppJobNameCMD.add(labels); + getAppJobNameCMD.add("--no-headers=true"); + getAppJobNameCMD.add("-o"); + + String customColumns = "custom-columns=" + + "NAME:.metadata.name"; + + getAppJobNameCMD.add(customColumns); + return getAppJobNameCMD; + } + protected Session constructSession(String k8sOutput) throws IOException { log.debug("line: " + k8sOutput); String[] parts = k8sOutput.trim().replaceAll("\\s+", " ").split(" "); @@ -736,6 +786,7 @@ protected Session constructSession(String k8sOutput) throws IOException { String name = parts[5]; String startTime = parts[6]; String deletionTimestamp = parts[7]; + String appID = parts[8]; if (deletionTimestamp != null && !NONE.equals(deletionTimestamp)) { status = Session.STATUS_TERMINATING; } @@ -761,10 +812,12 @@ protected Session constructSession(String k8sOutput) throws IOException { } Session session = new Session(id, userid, image, type, status, name, startTime, connectURL); - if (parts.length > 8) { - String requestedRAM = parts[8]; - String requestedCPUCores = parts[9]; - String requestedGPUCores = parts[10]; + session.setAppId(appID); + + if (parts.length > 9) { + String requestedRAM = parts[9]; + String requestedCPUCores = parts[10]; + String requestedGPUCores = parts[11]; session.setRequestedRAM(toCommonUnit(requestedRAM)); session.setRequestedCPUCores(toCoreUnit(requestedCPUCores)); session.setRequestedGPUCores(toCoreUnit(requestedGPUCores)); diff --git a/skaha/src/main/webapp/service.yaml b/skaha/src/main/webapp/service.yaml index d475078f..d0f9d555 100644 --- a/skaha/src/main/webapp/service.yaml +++ b/skaha/src/main/webapp/service.yaml @@ -202,7 +202,31 @@ paths: description: Service busy default: description: Unexpected error - /v0/session/{sessionID}/app: + /session/{sessionID}/app: + get: + description: | + List the desktop apps for the calling user, returned as a JSON array. Desktop app attributes include:

Desktop Session ID
App ID
User ID
Image
Type
Status
Name
StartTime
Connect URL

Valid type is 'desktop-app'. + tags: + - Session Management + parameters: + - name: status + in: query + type: string + description: Only show sessions with this status (Pending, Running, Terminating, Succeeded, Error) + required: false + responses: + '200': + description: Successful response + '401': + description: Not authenticated + '403': + description: Permission denied + '500': + description: Internal error + '503': + description: Service busy + default: + description: Unexpected error post: description: | Attach a desktop-app to the session identified by sessionID. This only applies to sessions of type 'desktop'. @@ -219,6 +243,79 @@ paths: type: string description: The imageID of the desktop-app to attach required: true + - name: cores + in: query + type: string + description: | + Request this many cores for the desktop app. This value must match a value retruned from the /context endpoint. The default value returned from /context will be used if this parameter is not provided. + required: false + - name: ram + in: query + type: string + description: | + Request this much RAM (GB) for the desktop app. This value must match a value retruned from the /context endpoint. The default value returned from /context will be used if this parameter is not provided. + required: false + responses: + '200': + description: Successful response + '401': + description: Not authenticated + '403': + description: Permission denied + '500': + description: Internal error + '503': + description: Service busy + default: + description: Unexpected error + /session/{sessionID}/app/{appID}: + get: + description: | + Get the desktop app identified by sessionID and appID as a JSON object. + tags: + - Session Management + parameters: + - name: sessionID + in: path + type: string + description: Session ID of the desktop app. + required: true + - name: appID + in: path + type: string + description: ID of the desktop app to get, returned in application/json format. + required: true + responses: + '200': + description: Successful response + '401': + description: Not authenticated + '404': + description: Not found + '403': + description: Permission denied + '500': + description: Internal error + '503': + description: Service busy + default: + description: Unexpected error + delete: + description: | + Delete the desktop app identified by sessionID and appID. + tags: + - Session Management + parameters: + - name: sessionID + in: path + type: string + description: Session ID of the desktop app to delete. + required: true + - name: appID + in: path + type: string + description: ID of the desktop app to delete. + required: true responses: '200': description: Successful response diff --git a/skaha/src/test/java/org/opencadc/skaha/session/GetSessionsTests.java b/skaha/src/test/java/org/opencadc/skaha/session/GetSessionsTests.java index 6d36af58..17258a09 100644 --- a/skaha/src/test/java/org/opencadc/skaha/session/GetSessionsTests.java +++ b/skaha/src/test/java/org/opencadc/skaha/session/GetSessionsTests.java @@ -95,11 +95,11 @@ public class GetSessionsTests { } private static final String K8S_LIST = - "pud05npw majorb imageID carta Running brian 2021-02-02T17:49:55Z \n" + - "e37lmx4m majorb imageID desktop Terminating brian 2021-01-28T21:52:51Z \n" + - "gspc0n8m majorb imageID notebook Running brian 2021-01-29T22:56:21Z \n" + - "abcd0n8m majorb imageID notebook Terminating brian 2021-01-29T22:56:21Z \n" + - "defg0n8m majorb imageID notebook Running brian 2021-01-29T22:56:21Z \n"; + "pud05npw majorb imageID carta Running brian 2021-02-02T17:49:55Z \n" + + "e37lmx4m majorb imageID desktop Terminating brian 2021-01-28T21:52:51Z \n" + + "gspc0n8m majorb imageID notebook Running brian 2021-01-29T22:56:21Z \n" + + "abcd0n8m majorb imageID notebook Terminating brian 2021-01-29T22:56:21Z \n" + + "defg0n8m majorb imageID notebook Running brian 2021-01-29T22:56:21Z \n"; public GetSessionsTests() { }