From 5ee71f835248d14015832348f9000c9761447d2e Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Wed, 22 Jan 2025 13:28:08 -0800 Subject: [PATCH 1/5] feat: add configuration for separate hostname for sessions in workload --- .../main/java/org/opencadc/skaha/K8SUtil.java | 42 ++++++++++++++++++- .../java/org/opencadc/skaha/SkahaAction.java | 4 +- .../opencadc/skaha/session/PostAction.java | 7 ++-- .../opencadc/skaha/session/SessionAction.java | 24 ----------- .../opencadc/skaha/session/SessionDAO.java | 14 ++----- 5 files changed, 49 insertions(+), 42 deletions(-) diff --git a/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java b/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java index 66700ca5b..19a6e008a 100644 --- a/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java +++ b/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java @@ -71,13 +71,20 @@ import org.apache.log4j.Logger; public class K8SUtil { + private static final String VNC_URL_TEMPLATE = + "https://%s/session/desktop/%s/?password=%s&path=session/desktop/%s/"; + private static final String CARTA_URL_TEMPLATE = "https://%s/session/carta/http/%s/"; + private static final String CARTA_ALT_SOCKET_URL_TEMPLATE = + K8SUtil.CARTA_URL_TEMPLATE + "?socketUrl=wss://%s/session/carta/ws/%s/"; + private static final String NOTEBOOK_URL_TEMPLATE = "https://%s/session/notebook/%s/lab/tree/%s/home/%s?token=%s"; + private static final String CONTRIBUTED_URL_TEMPLATE = "https://%s/session/contrib/%s/"; static final String ARC_USER_QUOTA_IN_GB_NAME = "skaha.defaultquotagb"; private static final Logger log = Logger.getLogger(K8SUtil.class); - public static String getHostName() { - return System.getenv("skaha.hostname"); + public static String getSessionsHostName() { + return System.getenv("SKAHA_SESSIONS_HOSTNAME"); } public static String getWorkloadNamespace() { @@ -203,4 +210,35 @@ public static String getRedisPort() { public static String getUserHome() { return System.getProperty("user.home"); } + + public static String getVNCURL(String sessionID) { + return String.format(K8SUtil.VNC_URL_TEMPLATE, K8SUtil.getSessionsHostName(), sessionID, sessionID, sessionID); + } + + public static String getCartaURL(String sessionID, boolean altSocketUrl) { + final String host = K8SUtil.getSessionsHostName(); + return altSocketUrl ? K8SUtil.getCartaAltSocketURL(host, sessionID) : K8SUtil.getCartaURL(host, sessionID); + } + + private static String getCartaURL(final String host, final String sessionID) { + return String.format(K8SUtil.CARTA_URL_TEMPLATE, host, sessionID); + } + + private static String getCartaAltSocketURL(final String host, final String sessionID) { + return String.format(K8SUtil.CARTA_ALT_SOCKET_URL_TEMPLATE, host, sessionID, host, sessionID); + } + + public static String getNotebookURL(String sessionID, String userid, String skahaTLD) { + return String.format( + K8SUtil.NOTEBOOK_URL_TEMPLATE, + K8SUtil.getSessionsHostName(), + sessionID, + skahaTLD.replaceAll("/", ""), + userid, + sessionID); + } + + public static String getContributedURL(String sessionID) { + return String.format(K8SUtil.CONTRIBUTED_URL_TEMPLATE, K8SUtil.getSessionsHostName(), sessionID); + } } diff --git a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java index 57870986e..6bc5b1e8b 100644 --- a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java @@ -128,7 +128,6 @@ public abstract class SkahaAction extends RestAction { protected boolean adminUser = false; protected boolean headlessUser = false; protected boolean priorityHeadlessUser = false; - protected String server; protected String homedir; protected String scratchdir; protected String skahaTld; @@ -145,7 +144,6 @@ public abstract class SkahaAction extends RestAction { protected String callbackSupplementalGroups = null; public SkahaAction() { - server = K8SUtil.getHostName(); homedir = K8SUtil.getHomeDir(); skahaTld = K8SUtil.getSkahaTld(); gpuEnabled = K8SUtil.isGpuEnabled(); @@ -163,7 +161,7 @@ public SkahaAction() { final String configuredPosixMapperResourceID = K8SUtil.getPosixMapperResourceId(); - log.debug("skaha.hostname=" + server); + log.debug("skaha.hostname=" + K8SUtil.getSessionsHostName()); log.debug("skaha.homedir=" + homedir); log.debug("SKAHA_TLD=" + skahaTld); log.debug("skaha.scratchdir=" + scratchdir); 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 a4be60cea..30b22e8d0 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java @@ -605,7 +605,7 @@ public void createSession( .withParameter(PostAction.SKAHA_SESSIONNAME, name.toLowerCase()) .withParameter(PostAction.SKAHA_SESSIONEXPIRY, K8SUtil.getSessionExpiry()) .withParameter(PostAction.SKAHA_JOBNAME, jobName) - .withParameter(PostAction.SKAHA_HOSTNAME, K8SUtil.getHostName()) + .withParameter(PostAction.SKAHA_HOSTNAME, K8SUtil.getSessionsHostName()) .withParameter(PostAction.SKAHA_USERID, getUsername()) .withParameter(PostAction.SKAHA_POSIXID, Integer.toString(this.posixPrincipal.getUidNumber())) .withParameter(PostAction.SKAHA_SESSIONTYPE, type) @@ -662,7 +662,8 @@ public void createSession( byte[] ingressBytes = Files.readAllBytes(Paths.get(ingressPath)); String ingressString = new String(ingressBytes, StandardCharsets.UTF_8); ingressString = SessionJobBuilder.setConfigValue(ingressString, SKAHA_SESSIONID, sessionID); - ingressString = SessionJobBuilder.setConfigValue(ingressString, SKAHA_HOSTNAME, K8SUtil.getHostName()); + ingressString = + SessionJobBuilder.setConfigValue(ingressString, SKAHA_HOSTNAME, K8SUtil.getSessionsHostName()); jsonLaunchFile = super.stageFile(ingressString); launchCmd = KubectlCommandBuilder.command("create") .namespace(k8sNamespace) @@ -796,7 +797,7 @@ public void attachDesktopApp( .withParameter(PostAction.SKAHA_SESSIONID, this.sessionID) .withParameter(PostAction.SKAHA_SESSIONEXPIRY, K8SUtil.getSessionExpiry()) .withParameter(PostAction.SKAHA_SESSIONTYPE, SessionAction.TYPE_DESKTOP_APP) - .withParameter(PostAction.SKAHA_HOSTNAME, K8SUtil.getHostName()) + .withParameter(PostAction.SKAHA_HOSTNAME, K8SUtil.getSessionsHostName()) .withParameter(PostAction.SKAHA_USERID, getUsername()) .withParameter(PostAction.SKAHA_POSIXID, Integer.toString(this.posixPrincipal.getUidNumber())) .withParameter(PostAction.SOFTWARE_IMAGEID, image) 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 af63edbef..acb4cfbc7 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java @@ -112,30 +112,6 @@ public SessionAction() { super(); } - public static String getVNCURL(String host, String sessionID) { - // vnc.html does not... - return "https://" + host + "/session/desktop/" + sessionID + "/?password=" + sessionID - + "&path=session/desktop/" + sessionID + "/"; - } - - public static String getCartaURL(String host, String sessionID, boolean altSocketUrl) { - String url = "https://" + host + "/session/carta/http/" + sessionID + "/"; - if (altSocketUrl) { - url = url + "?socketUrl=wss://" + host + "/session/carta/ws/" + sessionID + "/"; - } - return url; - } - - public static String getNotebookURL(String host, String sessionID, String userid, String skahaTLD) { - return String.format( - "https://%s/session/notebook/%s/lab/tree/%s/home/%s?token=%s", - host, sessionID, skahaTLD.replaceAll("/", ""), userid, sessionID); - } - - public static String getContributedURL(String host, String sessionID) { - return "https://" + host + "/session/contrib/" + sessionID + "/"; - } - protected void initRequest() throws Exception { super.initRequest(); diff --git a/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java b/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java index 5a966c544..b5af802df 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java @@ -341,22 +341,16 @@ static Session constructSession(String k8sOutput, final String topLevelDirectory final String status = (deletionTimestamp != null && !NONE.equals(deletionTimestamp)) ? Session.STATUS_TERMINATING : parts[allColumns.indexOf(CustomColumns.STATUS)]; - final String host = K8SUtil.getHostName(); final String connectURL; if (SessionAction.SESSION_TYPE_DESKTOP.equals(type)) { - connectURL = SessionAction.getVNCURL(host, id); + connectURL = K8SUtil.getVNCURL(id); } else if (SessionAction.SESSION_TYPE_CARTA.equals(type)) { - if (image.endsWith(":1.4")) { - // support alt web socket path for 1.4 carta - connectURL = SessionAction.getCartaURL(host, id, true); - } else { - connectURL = SessionAction.getCartaURL(host, id, false); - } + connectURL = K8SUtil.getCartaURL(id, image.endsWith(":1.4")); } else if (SessionAction.SESSION_TYPE_NOTEBOOK.equals(type)) { - connectURL = SessionAction.getNotebookURL(host, id, userid, topLevelDirectory); + connectURL = K8SUtil.getNotebookURL(id, userid, topLevelDirectory); } else if (SessionAction.SESSION_TYPE_CONTRIB.equals(type)) { - connectURL = SessionAction.getContributedURL(host, id); + connectURL = K8SUtil.getContributedURL(id); } else { connectURL = "not-applicable"; } From d76e1bd676d45468afb49b47990f9c10258c8bf9 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Wed, 22 Jan 2025 13:53:54 -0800 Subject: [PATCH 2/5] refactor: rework for testing --- .../main/java/org/opencadc/skaha/K8SUtil.java | 34 +++++---- .../opencadc/skaha/session/SessionDAO.java | 9 +-- .../java/org/opencadc/skaha/K8SUtilTest.java | 71 +++++++++++++++++++ 3 files changed, 97 insertions(+), 17 deletions(-) diff --git a/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java b/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java index 19a6e008a..4cfd9e2c6 100644 --- a/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java +++ b/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java @@ -68,6 +68,7 @@ package org.opencadc.skaha; import java.util.List; +import java.util.Objects; import org.apache.log4j.Logger; public class K8SUtil { @@ -211,34 +212,41 @@ public static String getUserHome() { return System.getProperty("user.home"); } - public static String getVNCURL(String sessionID) { - return String.format(K8SUtil.VNC_URL_TEMPLATE, K8SUtil.getSessionsHostName(), sessionID, sessionID, sessionID); + public static String getVNCURL(String host, String sessionID) { + final String cleanSessionID = Objects.requireNonNull(sessionID); + return String.format( + K8SUtil.VNC_URL_TEMPLATE, Objects.requireNonNull(host), cleanSessionID, cleanSessionID, cleanSessionID); } - public static String getCartaURL(String sessionID, boolean altSocketUrl) { - final String host = K8SUtil.getSessionsHostName(); + public static String getCartaURL(String host, String sessionID, boolean altSocketUrl) { return altSocketUrl ? K8SUtil.getCartaAltSocketURL(host, sessionID) : K8SUtil.getCartaURL(host, sessionID); } private static String getCartaURL(final String host, final String sessionID) { - return String.format(K8SUtil.CARTA_URL_TEMPLATE, host, sessionID); + return String.format( + K8SUtil.CARTA_URL_TEMPLATE, Objects.requireNonNull(host), Objects.requireNonNull(sessionID)); } private static String getCartaAltSocketURL(final String host, final String sessionID) { - return String.format(K8SUtil.CARTA_ALT_SOCKET_URL_TEMPLATE, host, sessionID, host, sessionID); + final String cleanSessionID = Objects.requireNonNull(sessionID); + final String cleanHost = Objects.requireNonNull(host); + return String.format( + K8SUtil.CARTA_ALT_SOCKET_URL_TEMPLATE, cleanHost, cleanSessionID, cleanHost, cleanSessionID); } - public static String getNotebookURL(String sessionID, String userid, String skahaTLD) { + public static String getNotebookURL(String host, String sessionID, String userid, String skahaTLD) { + final String cleanSessionID = Objects.requireNonNull(sessionID); return String.format( K8SUtil.NOTEBOOK_URL_TEMPLATE, - K8SUtil.getSessionsHostName(), - sessionID, + Objects.requireNonNull(host), + cleanSessionID, skahaTLD.replaceAll("/", ""), - userid, - sessionID); + Objects.requireNonNull(userid), + cleanSessionID); } - public static String getContributedURL(String sessionID) { - return String.format(K8SUtil.CONTRIBUTED_URL_TEMPLATE, K8SUtil.getSessionsHostName(), sessionID); + public static String getContributedURL(String host, String sessionID) { + return String.format( + K8SUtil.CONTRIBUTED_URL_TEMPLATE, Objects.requireNonNull(host), Objects.requireNonNull(sessionID)); } } diff --git a/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java b/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java index b5af802df..6a699ada6 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java @@ -338,19 +338,20 @@ static Session constructSession(String k8sOutput, final String topLevelDirectory String image = parts[allColumns.indexOf(CustomColumns.IMAGE)]; String type = parts[allColumns.indexOf(CustomColumns.TYPE)]; String deletionTimestamp = parts[allColumns.indexOf(CustomColumns.DELETION)]; + final String sessionHostName = K8SUtil.getSessionsHostName(); final String status = (deletionTimestamp != null && !NONE.equals(deletionTimestamp)) ? Session.STATUS_TERMINATING : parts[allColumns.indexOf(CustomColumns.STATUS)]; final String connectURL; if (SessionAction.SESSION_TYPE_DESKTOP.equals(type)) { - connectURL = K8SUtil.getVNCURL(id); + connectURL = K8SUtil.getVNCURL(sessionHostName, id); } else if (SessionAction.SESSION_TYPE_CARTA.equals(type)) { - connectURL = K8SUtil.getCartaURL(id, image.endsWith(":1.4")); + connectURL = K8SUtil.getCartaURL(sessionHostName, id, image.endsWith(":1.4")); } else if (SessionAction.SESSION_TYPE_NOTEBOOK.equals(type)) { - connectURL = K8SUtil.getNotebookURL(id, userid, topLevelDirectory); + connectURL = K8SUtil.getNotebookURL(sessionHostName, id, userid, topLevelDirectory); } else if (SessionAction.SESSION_TYPE_CONTRIB.equals(type)) { - connectURL = K8SUtil.getContributedURL(id); + connectURL = K8SUtil.getContributedURL(sessionHostName, id); } else { connectURL = "not-applicable"; } diff --git a/skaha/src/test/java/org/opencadc/skaha/K8SUtilTest.java b/skaha/src/test/java/org/opencadc/skaha/K8SUtilTest.java index fbbf918ab..393bd68fe 100644 --- a/skaha/src/test/java/org/opencadc/skaha/K8SUtilTest.java +++ b/skaha/src/test/java/org/opencadc/skaha/K8SUtilTest.java @@ -80,4 +80,75 @@ public void sanitizeJobName() { Assert.assertEquals("Wrong name", "skaha-type-my-us-e-r-sess", K8SUtil.getJobName("SESS", "TYPE", "my|us+e&r")); } + + @Test + public void getVNCURL() { + Assert.assertEquals( + "Wrong URL", + "https://host.example.org/session/desktop/8675309/?password=8675309&path=session/desktop/8675309/", + K8SUtil.getVNCURL("host.example.org", "8675309")); + + try { + K8SUtil.getVNCURL(null, "8675309"); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException nullPointerException) { + // Good. + } + } + + @Test + public void getCartaURL() { + Assert.assertEquals( + "Wrong URL", + "https://host.example.org/session/carta/http/8675309/", + K8SUtil.getCartaURL("host.example.org", "8675309", false)); + Assert.assertEquals( + "Wrong URL", + "https://host.example.org/session/carta/http/8675309/?socketUrl=wss://host.example.org/session/carta/ws/8675309/", + K8SUtil.getCartaURL("host.example.org", "8675309", true)); + + try { + K8SUtil.getCartaURL(null, "8675309", false); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException nullPointerException) { + // Good. + } + } + + @Test + public void getNotebookURL() { + Assert.assertEquals( + "Wrong URL", + "https://host.example.org/session/notebook/8675309/lab/tree/top-level-dir/home/user?token=8675309", + K8SUtil.getNotebookURL("host.example.org", "8675309", "user", "/top-level-dir")); + + try { + K8SUtil.getNotebookURL(null, "8675309", "user", "/top-level-dir"); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException nullPointerException) { + // Good. + } + + try { + K8SUtil.getNotebookURL("host.example.org", "8675309", null, "/top-level-dir"); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException nullPointerException) { + // Good. + } + } + + @Test + public void getContributedURL() { + Assert.assertEquals( + "Wrong URL", + "https://host.example.org/session/contrib/8675309/", + K8SUtil.getContributedURL("host.example.org", "8675309")); + + try { + K8SUtil.getContributedURL(null, "8675309"); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException nullPointerException) { + // Good. + } + } } From f622097b29ed57aaedcde9e92388d37babc42afb Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Wed, 22 Jan 2025 14:13:08 -0800 Subject: [PATCH 3/5] test: test fixes with small api change --- deployment/helm/README.md | 5 +++++ .../main/java/org/opencadc/skaha/session/SessionDAO.java | 7 +++---- .../java/org/opencadc/skaha/session/GetActionTests.java | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/deployment/helm/README.md b/deployment/helm/README.md index f734a7c9f..f7fb7a538 100644 --- a/deployment/helm/README.md +++ b/deployment/helm/README.md @@ -315,6 +315,11 @@ deployment: maxCount: "3" # Max number of sessions per user. minEphemeralStorage: "20Gi" # The initial requested amount of ephemeral (local) storage. Does NOT apply to Desktop sessions. maxEphemeralStorage: "200Gi" # The maximum amount of ephemeral (local) storage to allow a Session to extend to. Does NOT apply to Desktop sessions. + + # Optionally setup a separate host for User Sessions for Skaha to redirect to. The HTTPS scheme is assumed. Defaults to the Skaha hostname (.Values.deployment.hostname). + # Example: + # hostname: myhost.example.org + hostname: sessions.example.org # When set to 'true' this flag will enable GPU node scheduling. Don't forget to declare any related GPU configurations, if appropriate, in the nodeAffinity below! # gpuEnabled: false diff --git a/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java b/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java index 6a699ada6..6ba10e506 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java @@ -75,7 +75,7 @@ public static Session getSession(String forUserID, String sessionID, final Strin throw new ResourceNotFoundException("session " + sessionID + " not found"); } - protected static List getSessions(String forUserID, String sessionID, final String topLevelDirectory) + static List getSessions(String forUserID, String sessionID, final String topLevelDirectory) throws Exception { String k8sNamespace = K8SUtil.getWorkloadNamespace(); String[] sessionsCMD = SessionDAO.getSessionsCMD(k8sNamespace, forUserID, sessionID); @@ -93,7 +93,7 @@ protected static List getSessions(String forUserID, String sessionID, f String[] lines = sessionList.split("\n"); for (String line : lines) { - Session session = SessionDAO.constructSession(line, topLevelDirectory); + Session session = SessionDAO.constructSession(K8SUtil.getSessionsHostName(), line, topLevelDirectory); if (forUserID != null) { // get expiry time String uid = getUID(line); @@ -325,7 +325,7 @@ private static Map getJobExpiryTimes(String k8sNamespace, String return jobExpiryTimes; } - static Session constructSession(String k8sOutput, final String topLevelDirectory) throws IOException { + static Session constructSession(String sessionHostName, String k8sOutput, final String topLevelDirectory) { LOGGER.debug("line: " + k8sOutput); final List allColumns = Arrays.asList(CustomColumns.values()); @@ -338,7 +338,6 @@ static Session constructSession(String k8sOutput, final String topLevelDirectory String image = parts[allColumns.indexOf(CustomColumns.IMAGE)]; String type = parts[allColumns.indexOf(CustomColumns.TYPE)]; String deletionTimestamp = parts[allColumns.indexOf(CustomColumns.DELETION)]; - final String sessionHostName = K8SUtil.getSessionsHostName(); final String status = (deletionTimestamp != null && !NONE.equals(deletionTimestamp)) ? Session.STATUS_TERMINATING : parts[allColumns.indexOf(CustomColumns.STATUS)]; diff --git a/skaha/src/test/java/org/opencadc/skaha/session/GetActionTests.java b/skaha/src/test/java/org/opencadc/skaha/session/GetActionTests.java index aca7fd540..452cd6909 100644 --- a/skaha/src/test/java/org/opencadc/skaha/session/GetActionTests.java +++ b/skaha/src/test/java/org/opencadc/skaha/session/GetActionTests.java @@ -210,14 +210,14 @@ public void testFilterTypeStatus() { static class TestGetAction extends GetAction { @Override - public List getAllSessions(String forUserID) throws Exception { + public List getAllSessions(String forUserID) { // A bit of a hack to emulate the state. this.skahaTld = "/cavern-vospace"; List sessions = new ArrayList<>(); String[] lines = K8S_LIST.split("\n"); for (String line : lines) { - Session session = SessionDAO.constructSession(line, this.skahaTld); + Session session = SessionDAO.constructSession("host.example.org", line, this.skahaTld); sessions.add(session); } return sessions; From d40ca03381304f2a248378d86c5256acf50d4f04 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Thu, 23 Jan 2025 16:02:06 -0800 Subject: [PATCH 4/5] refactor: add url builder objects --- skaha/Dockerfile.local | 16 ++ skaha/build.gradle | 4 + .../main/java/org/opencadc/skaha/K8SUtil.java | 46 ---- .../opencadc/skaha/session/SessionDAO.java | 17 +- .../skaha/session/SessionURLBuilder.java | 259 ++++++++++++++++++ .../java/org/opencadc/skaha/K8SUtilTest.java | 71 ----- .../skaha/session/GetActionTests.java | 2 +- .../skaha/session/SessionURLBuilderTest.java | 100 +++++++ 8 files changed, 392 insertions(+), 123 deletions(-) create mode 100644 skaha/Dockerfile.local create mode 100644 skaha/src/main/java/org/opencadc/skaha/session/SessionURLBuilder.java create mode 100644 skaha/src/test/java/org/opencadc/skaha/session/SessionURLBuilderTest.java diff --git a/skaha/Dockerfile.local b/skaha/Dockerfile.local new file mode 100644 index 000000000..14edfb37b --- /dev/null +++ b/skaha/Dockerfile.local @@ -0,0 +1,16 @@ +FROM images.opencadc.org/library/cadc-tomcat:1.3 + +RUN set -eux \ + && dnf install --nodocs --assumeyes --setopt=install_weak_deps=False dnf-plugins-core-4.10.0-1.fc40 \ + && dnf -y config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo \ + && dnf -y install --nodocs --assumeyes --setopt=install_weak_deps=False \ + acl-2.3.2-1.fc40 attr-2.5.2-3.fc40 \ + containerd.io-1.7.22-3.1.fc40 \ + docker-ce-3:27.3.1-1.fc40 \ + docker-ce-cli-1:27.3.1-1.fc40 \ + kubernetes-client \ + # Clean up dnf cache and other unneeded files to reduce image size + && dnf clean all + +COPY ./build/libs/skaha.war /usr/share/tomcat/webapps/ +COPY ./src/scripts/* /usr/local/bin/ \ No newline at end of file diff --git a/skaha/build.gradle b/skaha/build.gradle index df3babd50..c48160e45 100644 --- a/skaha/build.gradle +++ b/skaha/build.gradle @@ -35,6 +35,10 @@ tasks.withType(Copy).configureEach { dependencies { providedCompile 'javax.servlet:javax.servlet-api:[3.1.0,)' + // https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5 + // Used to assemble URIs programmatically + implementation 'org.apache.httpcomponents.client5:httpclient5:5.4.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2' implementation 'com.google.code.gson:gson:2.8.9' implementation 'commons-io:commons-io:[2.14.0,3.0.0)' diff --git a/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java b/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java index 4cfd9e2c6..651f665e2 100644 --- a/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java +++ b/skaha/src/main/java/org/opencadc/skaha/K8SUtil.java @@ -68,17 +68,9 @@ package org.opencadc.skaha; import java.util.List; -import java.util.Objects; import org.apache.log4j.Logger; public class K8SUtil { - private static final String VNC_URL_TEMPLATE = - "https://%s/session/desktop/%s/?password=%s&path=session/desktop/%s/"; - private static final String CARTA_URL_TEMPLATE = "https://%s/session/carta/http/%s/"; - private static final String CARTA_ALT_SOCKET_URL_TEMPLATE = - K8SUtil.CARTA_URL_TEMPLATE + "?socketUrl=wss://%s/session/carta/ws/%s/"; - private static final String NOTEBOOK_URL_TEMPLATE = "https://%s/session/notebook/%s/lab/tree/%s/home/%s?token=%s"; - private static final String CONTRIBUTED_URL_TEMPLATE = "https://%s/session/contrib/%s/"; static final String ARC_USER_QUOTA_IN_GB_NAME = "skaha.defaultquotagb"; @@ -211,42 +203,4 @@ public static String getRedisPort() { public static String getUserHome() { return System.getProperty("user.home"); } - - public static String getVNCURL(String host, String sessionID) { - final String cleanSessionID = Objects.requireNonNull(sessionID); - return String.format( - K8SUtil.VNC_URL_TEMPLATE, Objects.requireNonNull(host), cleanSessionID, cleanSessionID, cleanSessionID); - } - - public static String getCartaURL(String host, String sessionID, boolean altSocketUrl) { - return altSocketUrl ? K8SUtil.getCartaAltSocketURL(host, sessionID) : K8SUtil.getCartaURL(host, sessionID); - } - - private static String getCartaURL(final String host, final String sessionID) { - return String.format( - K8SUtil.CARTA_URL_TEMPLATE, Objects.requireNonNull(host), Objects.requireNonNull(sessionID)); - } - - private static String getCartaAltSocketURL(final String host, final String sessionID) { - final String cleanSessionID = Objects.requireNonNull(sessionID); - final String cleanHost = Objects.requireNonNull(host); - return String.format( - K8SUtil.CARTA_ALT_SOCKET_URL_TEMPLATE, cleanHost, cleanSessionID, cleanHost, cleanSessionID); - } - - public static String getNotebookURL(String host, String sessionID, String userid, String skahaTLD) { - final String cleanSessionID = Objects.requireNonNull(sessionID); - return String.format( - K8SUtil.NOTEBOOK_URL_TEMPLATE, - Objects.requireNonNull(host), - cleanSessionID, - skahaTLD.replaceAll("/", ""), - Objects.requireNonNull(userid), - cleanSessionID); - } - - public static String getContributedURL(String host, String sessionID) { - return String.format( - K8SUtil.CONTRIBUTED_URL_TEMPLATE, Objects.requireNonNull(host), Objects.requireNonNull(sessionID)); - } } diff --git a/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java b/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java index 6ba10e506..ab5dc0ee4 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/SessionDAO.java @@ -325,7 +325,8 @@ private static Map getJobExpiryTimes(String k8sNamespace, String return jobExpiryTimes; } - static Session constructSession(String sessionHostName, String k8sOutput, final String topLevelDirectory) { + static Session constructSession(String sessionHostName, String k8sOutput, final String topLevelDirectory) + throws Exception { LOGGER.debug("line: " + k8sOutput); final List allColumns = Arrays.asList(CustomColumns.values()); @@ -344,13 +345,19 @@ static Session constructSession(String sessionHostName, String k8sOutput, final final String connectURL; if (SessionAction.SESSION_TYPE_DESKTOP.equals(type)) { - connectURL = K8SUtil.getVNCURL(sessionHostName, id); + connectURL = SessionURLBuilder.vncSession(sessionHostName, id).build(); } else if (SessionAction.SESSION_TYPE_CARTA.equals(type)) { - connectURL = K8SUtil.getCartaURL(sessionHostName, id, image.endsWith(":1.4")); + connectURL = SessionURLBuilder.cartaSession(sessionHostName, id) + .withAlternateSocket(image.endsWith(":1.4")) + .build(); } else if (SessionAction.SESSION_TYPE_NOTEBOOK.equals(type)) { - connectURL = K8SUtil.getNotebookURL(sessionHostName, id, userid, topLevelDirectory); + connectURL = SessionURLBuilder.notebookSession(sessionHostName, id) + .withTopLevelDirectory(topLevelDirectory) + .withUserName(userid) + .build(); } else if (SessionAction.SESSION_TYPE_CONTRIB.equals(type)) { - connectURL = K8SUtil.getContributedURL(sessionHostName, id); + connectURL = + SessionURLBuilder.contributedSession(sessionHostName, id).build(); } else { connectURL = "not-applicable"; } diff --git a/skaha/src/main/java/org/opencadc/skaha/session/SessionURLBuilder.java b/skaha/src/main/java/org/opencadc/skaha/session/SessionURLBuilder.java new file mode 100644 index 000000000..09528f8df --- /dev/null +++ b/skaha/src/main/java/org/opencadc/skaha/session/SessionURLBuilder.java @@ -0,0 +1,259 @@ +package org.opencadc.skaha.session; + +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.apache.http.client.utils.URIBuilder; + +/** Build a URL to a session. */ +public abstract class SessionURLBuilder { + final String host; + final String sessionID; + + /** + * Constructor. Use the static factory methods to create instances. + * + * @param host The host name. + * @param sessionID The session ID. + */ + private SessionURLBuilder(final String host, final String sessionID) { + this.host = Objects.requireNonNull(host); + this.sessionID = Objects.requireNonNull(sessionID); + } + + /** + * Create a builder to a VNC (Desktop) session. + * + * @param host The host name. + * @param sessionID The session ID. + * @return VNCSessionURLBuilder instance. Never null. + */ + static VNCSessionURLBuilder vncSession(final String host, final String sessionID) { + return new VNCSessionURLBuilder(host, sessionID); + } + + /** + * Create a builder to a Notebook session. + * + * @param host The host name. + * @param sessionID The session ID. + * @return NotebookSessionURLBuilder instance. Never null. + */ + static NotebookSessionURLBuilder notebookSession(final String host, final String sessionID) { + return new NotebookSessionURLBuilder(host, sessionID); + } + + /** + * Create a builder to a Carta session. + * + * @param host The host name. + * @param sessionID The session ID. + * @return CartaSessionURLBuilder instance. Never null. + */ + static CartaSessionURLBuilder cartaSession(final String host, final String sessionID) { + return new CartaSessionURLBuilder(host, sessionID); + } + + /** + * Create a builder to a contributed session. + * + * @param host The host name. + * @param sessionID The session ID. + * @return ContributedSessionURLBuilder instance. Never null. + */ + static ContributedSessionURLBuilder contributedSession(final String host, final String sessionID) { + return new ContributedSessionURLBuilder(host, sessionID); + } + + /** + * Abstract method to build the URL. + * + * @return The URL string. Never null. + * @throws URISyntaxException If the URI cannot be created. + */ + abstract String build() throws URISyntaxException; + + /** Construct a URL for a VNC session to a running Desktop. Used to redirect the end user to the VNC viewer. */ + static final class VNCSessionURLBuilder extends SessionURLBuilder { + VNCSessionURLBuilder(String host, String sessionID) { + super(host, sessionID); + } + + /** + * Build the URL for a VNC session. Example output: + * https://host.example.org/session/desktop/8675309?password=8675309&path=session/desktop/8675309/ + * + * @return URL string in format + * https://${host}/session/desktop/${sessionID}?password=${sessionID}&path=session/desktop/${sessionID} + * + * @throws URISyntaxException If the URI cannot be created. + */ + @Override + String build() throws URISyntaxException { + return new URIBuilder() + .setScheme("https") + .setHost(this.host) + .setPathSegments("session", "desktop", this.sessionID, "") // Extra empty string to append slash + .setCustomQuery("password=" + this.sessionID + "&path=session/desktop/" + this.sessionID + "/") + .build() + .toString(); + } + } + + /** Construct a URL for a Notebook session. Used to redirect the end user to the Jupyter Notebook. */ + static final class NotebookSessionURLBuilder extends SessionURLBuilder { + private String topLevelDirectory; + private String userName; + + NotebookSessionURLBuilder(String host, String sessionID) { + super(host, sessionID); + } + + /** + * Set the top-level directory in the Jupyter Notebook session. + * + * @param topLevelDirectory The top-level directory in the Jupyter Notebook session. + * @return This builder. + */ + NotebookSessionURLBuilder withTopLevelDirectory(String topLevelDirectory) { + this.topLevelDirectory = topLevelDirectory; + return this; + } + + /** + * Set the user's name in the Jupyter Notebook session. + * + * @param userName The user's name in the Jupyter Notebook session. + * @return This builder. + */ + NotebookSessionURLBuilder withUserName(String userName) { + this.userName = userName; + return this; + } + + /** + * Build the URL for a Notebook session. Example output: + * https://host.example.org/session/notebook/8675309/lab/tree/top-level-dir/home/username?token=8675309 + * + * + * @return URL string in format + * https://${host}/session/notebook/${sessionID}/lab/tree/${topLevelDirInSkaha}/home/username?token=${sessionID} + * + * @throws URISyntaxException If the URI cannot be created. + */ + @Override + String build() throws URISyntaxException { + final Path topLevelDirectoryPath = Path.of(Objects.requireNonNull(this.topLevelDirectory)); + final List topLevelDirectoryPathSegments = new ArrayList<>(); + topLevelDirectoryPathSegments.add("session"); + topLevelDirectoryPathSegments.add("notebook"); + topLevelDirectoryPathSegments.add(this.sessionID); + topLevelDirectoryPathSegments.add("lab"); + topLevelDirectoryPathSegments.add("tree"); + topLevelDirectoryPath + .iterator() + .forEachRemaining(name -> topLevelDirectoryPathSegments.add(name.toString())); + topLevelDirectoryPathSegments.add("home"); + topLevelDirectoryPathSegments.add(Objects.requireNonNull(this.userName)); + + return new URIBuilder() + .setScheme("https") + .setHost(this.host) + .setPathSegments(topLevelDirectoryPathSegments) + .setParameter("token", this.sessionID) + .build() + .toString(); + } + } + + /** Construct a URL for a Carta session. Used to redirect the end user to the Carta viewer. */ + static final class CartaSessionURLBuilder extends SessionURLBuilder { + private boolean useAlternateSocketURL = false; + + /** + * Constructor. + * + * @param host The host name. + * @param sessionID The session ID. + */ + CartaSessionURLBuilder(String host, String sessionID) { + super(host, sessionID); + } + + /** + * Set the use of an alternate socket for the Carta session. + * + * @param useAlternateSocketURL Specify the alternate socket URL, omit it otherwise. + * @return This builder. + */ + CartaSessionURLBuilder withAlternateSocket(boolean useAlternateSocketURL) { + this.useAlternateSocketURL = useAlternateSocketURL; + return this; + } + + /** + * Build the URL for a Carta session. Example output: + * https://host.example.org/session/carta/8675309? + * or + * https://host.example.org/session/carta/8675309?socketUrl=wss://host.example.org/session/carta/ws/8675309/ + * + * + * @return URL string in format + * https://${host}/session/carta/${sessionID}?token=${sessionID} + * + * @throws URISyntaxException If the URI cannot be created. + */ + @Override + String build() throws URISyntaxException { + final URIBuilder builder = new URIBuilder() + .setScheme("https") + .setHost(this.host) + .setPathSegments("session", "carta", "http", this.sessionID, ""); + final URIBuilder uriBuilder; + + if (this.useAlternateSocketURL) { + uriBuilder = builder.setCustomQuery( + String.format("socketUrl=wss://%s/session/carta/ws/%s/", this.host, this.sessionID)); + } else { + uriBuilder = builder; + } + + return uriBuilder.build().toString(); + } + } + + /** Construct a URL for a contributed session. Used to redirect the end user to the contributed session. */ + static final class ContributedSessionURLBuilder extends SessionURLBuilder { + /** + * Constructor. + * + * @param host The host name. + * @param sessionID The session ID. + */ + ContributedSessionURLBuilder(String host, String sessionID) { + super(host, sessionID); + } + + /** + * Build the URL for a contributed session. Example output: + * https://host.example.org/session/contrib/8675309 + * + * + * @return URL string in format + * https://${host}/session/contrib/${sessionID} + * + * @throws URISyntaxException If the URI cannot be created. + */ + @Override + String build() throws URISyntaxException { + return new URIBuilder() + .setScheme("https") + .setHost(this.host) + .setPathSegments("session", "contrib", this.sessionID, "") + .build() + .toString(); + } + } +} diff --git a/skaha/src/test/java/org/opencadc/skaha/K8SUtilTest.java b/skaha/src/test/java/org/opencadc/skaha/K8SUtilTest.java index 393bd68fe..fbbf918ab 100644 --- a/skaha/src/test/java/org/opencadc/skaha/K8SUtilTest.java +++ b/skaha/src/test/java/org/opencadc/skaha/K8SUtilTest.java @@ -80,75 +80,4 @@ public void sanitizeJobName() { Assert.assertEquals("Wrong name", "skaha-type-my-us-e-r-sess", K8SUtil.getJobName("SESS", "TYPE", "my|us+e&r")); } - - @Test - public void getVNCURL() { - Assert.assertEquals( - "Wrong URL", - "https://host.example.org/session/desktop/8675309/?password=8675309&path=session/desktop/8675309/", - K8SUtil.getVNCURL("host.example.org", "8675309")); - - try { - K8SUtil.getVNCURL(null, "8675309"); - Assert.fail("Expected NullPointerException"); - } catch (NullPointerException nullPointerException) { - // Good. - } - } - - @Test - public void getCartaURL() { - Assert.assertEquals( - "Wrong URL", - "https://host.example.org/session/carta/http/8675309/", - K8SUtil.getCartaURL("host.example.org", "8675309", false)); - Assert.assertEquals( - "Wrong URL", - "https://host.example.org/session/carta/http/8675309/?socketUrl=wss://host.example.org/session/carta/ws/8675309/", - K8SUtil.getCartaURL("host.example.org", "8675309", true)); - - try { - K8SUtil.getCartaURL(null, "8675309", false); - Assert.fail("Expected NullPointerException"); - } catch (NullPointerException nullPointerException) { - // Good. - } - } - - @Test - public void getNotebookURL() { - Assert.assertEquals( - "Wrong URL", - "https://host.example.org/session/notebook/8675309/lab/tree/top-level-dir/home/user?token=8675309", - K8SUtil.getNotebookURL("host.example.org", "8675309", "user", "/top-level-dir")); - - try { - K8SUtil.getNotebookURL(null, "8675309", "user", "/top-level-dir"); - Assert.fail("Expected NullPointerException"); - } catch (NullPointerException nullPointerException) { - // Good. - } - - try { - K8SUtil.getNotebookURL("host.example.org", "8675309", null, "/top-level-dir"); - Assert.fail("Expected NullPointerException"); - } catch (NullPointerException nullPointerException) { - // Good. - } - } - - @Test - public void getContributedURL() { - Assert.assertEquals( - "Wrong URL", - "https://host.example.org/session/contrib/8675309/", - K8SUtil.getContributedURL("host.example.org", "8675309")); - - try { - K8SUtil.getContributedURL(null, "8675309"); - Assert.fail("Expected NullPointerException"); - } catch (NullPointerException nullPointerException) { - // Good. - } - } } diff --git a/skaha/src/test/java/org/opencadc/skaha/session/GetActionTests.java b/skaha/src/test/java/org/opencadc/skaha/session/GetActionTests.java index 452cd6909..f1cacf01c 100644 --- a/skaha/src/test/java/org/opencadc/skaha/session/GetActionTests.java +++ b/skaha/src/test/java/org/opencadc/skaha/session/GetActionTests.java @@ -210,7 +210,7 @@ public void testFilterTypeStatus() { static class TestGetAction extends GetAction { @Override - public List getAllSessions(String forUserID) { + public List getAllSessions(String forUserID) throws Exception { // A bit of a hack to emulate the state. this.skahaTld = "/cavern-vospace"; diff --git a/skaha/src/test/java/org/opencadc/skaha/session/SessionURLBuilderTest.java b/skaha/src/test/java/org/opencadc/skaha/session/SessionURLBuilderTest.java new file mode 100644 index 000000000..517bcae55 --- /dev/null +++ b/skaha/src/test/java/org/opencadc/skaha/session/SessionURLBuilderTest.java @@ -0,0 +1,100 @@ +package org.opencadc.skaha.session; + +import org.junit.Assert; +import org.junit.Test; + +public class SessionURLBuilderTest { + @Test + public void testVNCSession() throws Exception { + final String vncURL = + SessionURLBuilder.vncSession("host.example.org", "8675309").build(); + + Assert.assertEquals( + "Wrong URL", + "https://host.example.org/session/desktop/8675309/?password=8675309&path=session/desktop/8675309/", + vncURL); + + try { + SessionURLBuilder.vncSession(null, "8675309").build(); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException e) { + // Expected + } + } + + @Test + public void testNotebookSession() throws Exception { + final SessionURLBuilder.NotebookSessionURLBuilder testSubject = + SessionURLBuilder.notebookSession("host.example.org", "8675309"); + + try { + testSubject.build(); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException nullPointerException) { + // Good. + } + + final SessionURLBuilder.NotebookSessionURLBuilder testSubjectWithTLD = + testSubject.withTopLevelDirectory("/top-level-dir/sub-dir"); + + try { + testSubjectWithTLD.build(); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException nullPointerException) { + // Good. + } + + final SessionURLBuilder.NotebookSessionURLBuilder testSubjectWithUserName = + testSubjectWithTLD.withUserName("username"); + + Assert.assertEquals( + "Wrong Notebook URL", + "https://host.example.org/session/notebook/8675309/lab/tree/top-level-dir/sub-dir/home/username?token=8675309", + testSubjectWithUserName.build()); + + try { + SessionURLBuilder.notebookSession(null, "8675309").build(); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException e) { + // Expected + } + } + + @Test + public void testCartaSession() throws Exception { + final SessionURLBuilder.CartaSessionURLBuilder testSubject = + SessionURLBuilder.cartaSession("host.example.org", "8675309"); + + Assert.assertEquals( + "Wrong Carta URL", "https://host.example.org/session/carta/http/8675309/", testSubject.build()); + + final SessionURLBuilder.CartaSessionURLBuilder testSubjectWithSocketUrl = testSubject.withAlternateSocket(true); + + Assert.assertEquals( + "Wrong Carta URL", + "https://host.example.org/session/carta/http/8675309/?socketUrl=wss://host.example.org/session/carta/ws/8675309/", + testSubjectWithSocketUrl.build()); + + try { + SessionURLBuilder.cartaSession(null, "8675309").build(); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException e) { + // Expected + } + } + + @Test + public void testContributedSession() throws Exception { + final String contributedURL = SessionURLBuilder.contributedSession("host.example.org", "8675309") + .build(); + + Assert.assertEquals("Wrong URL", "https://host.example.org/session/contrib/8675309/", contributedURL); + + try { + SessionURLBuilder.contributedSession(null, "8675309").build(); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException e) { + // Expected + } + } +} From 0ba1a2a67fb13621ee4128ab63a7bf4cd2cc3cbb Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Thu, 23 Jan 2025 16:11:19 -0800 Subject: [PATCH 5/5] fix: remove unused dockerfile --- skaha/Dockerfile.local | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 skaha/Dockerfile.local diff --git a/skaha/Dockerfile.local b/skaha/Dockerfile.local deleted file mode 100644 index 14edfb37b..000000000 --- a/skaha/Dockerfile.local +++ /dev/null @@ -1,16 +0,0 @@ -FROM images.opencadc.org/library/cadc-tomcat:1.3 - -RUN set -eux \ - && dnf install --nodocs --assumeyes --setopt=install_weak_deps=False dnf-plugins-core-4.10.0-1.fc40 \ - && dnf -y config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo \ - && dnf -y install --nodocs --assumeyes --setopt=install_weak_deps=False \ - acl-2.3.2-1.fc40 attr-2.5.2-3.fc40 \ - containerd.io-1.7.22-3.1.fc40 \ - docker-ce-3:27.3.1-1.fc40 \ - docker-ce-cli-1:27.3.1-1.fc40 \ - kubernetes-client \ - # Clean up dnf cache and other unneeded files to reduce image size - && dnf clean all - -COPY ./build/libs/skaha.war /usr/share/tomcat/webapps/ -COPY ./src/scripts/* /usr/local/bin/ \ No newline at end of file