This is an automated email from the ASF dual-hosted git repository.
dimuthuupe pushed a commit to branch cybershuttle-staging
in repository https://gitbox.apache.org/repos/asf/airavata.git
The following commit(s) were added to refs/heads/cybershuttle-staging by this
push:
new df26dd3e92 Support session termination & deletion on jupyter
df26dd3e92 is described below
commit df26dd3e922c6169d81d4950e26287059485d18e
Author: ganning127 <[email protected]>
AuthorDate: Thu Apr 10 22:58:40 2025 -0400
Support session termination & deletion on jupyter
---
modules/research-framework/portal/src/App.tsx | 2 +-
.../auth/{Login.tsx => UserLoginPage.tsx} | 0
.../portal/src/components/home/SessionCard.tsx | 18 +++---
.../home/StartSessionFromProjectButton.tsx | 13 +++-
.../service/config/DevDataInitializer.java | 2 +-
.../controller/GlobalExceptionController.java | 12 +++-
.../service/handlers/ResearchHubHandler.java | 75 +++++++++++++++++++++-
.../research/service/handlers/SessionHandler.java | 33 +++++++---
.../service/model/repo/SessionRepository.java | 2 +
.../src/main/resources/application.yml | 11 ++--
10 files changed, 137 insertions(+), 31 deletions(-)
diff --git a/modules/research-framework/portal/src/App.tsx
b/modules/research-framework/portal/src/App.tsx
index 028a5ed28a..5433a82ec0 100644
--- a/modules/research-framework/portal/src/App.tsx
+++ b/modules/research-framework/portal/src/App.tsx
@@ -6,7 +6,7 @@ import { Datasets } from "./components/datasets";
import ResourceDetails from "./components/resources/ResourceDetails";
import Notebooks from "./components/notebooks";
import Repositories from "./components/repositories";
-import { Login } from "./components/auth/Login";
+import { Login } from "./components/auth/UserLoginPage";
import ProtectedComponent from "./components/auth/ProtectedComponent";
import { AuthProvider, AuthProviderProps } from "react-oidc-context";
import { useEffect, useState } from "react";
diff --git a/modules/research-framework/portal/src/components/auth/Login.tsx
b/modules/research-framework/portal/src/components/auth/UserLoginPage.tsx
similarity index 100%
rename from modules/research-framework/portal/src/components/auth/Login.tsx
rename to
modules/research-framework/portal/src/components/auth/UserLoginPage.tsx
diff --git
a/modules/research-framework/portal/src/components/home/SessionCard.tsx
b/modules/research-framework/portal/src/components/home/SessionCard.tsx
index 96123a000d..2bd14a2d95 100644
--- a/modules/research-framework/portal/src/components/home/SessionCard.tsx
+++ b/modules/research-framework/portal/src/components/home/SessionCard.tsx
@@ -116,14 +116,16 @@ export const SessionCard = ({ session }: { session:
SessionType }) => {
<Heading size="lg">{session.sessionName}</Heading>
</Box>
<VStack alignItems="flex-end">
- <IconButton
- color="red.600"
- size="xs"
- variant={"ghost"}
- onClick={() => dialog.setOpen(true)}
- >
- <FaTrash />
- </IconButton>
+ {session.status === SessionStatusEnum.TERMINATED && (
+ <IconButton
+ color="red.600"
+ size="xs"
+ variant={"ghost"}
+ onClick={() => dialog.setOpen(true)}
+ >
+ <FaTrash />
+ </IconButton>
+ )}
</VStack>
</HStack>
</Card.Header>
diff --git
a/modules/research-framework/portal/src/components/home/StartSessionFromProjectButton.tsx
b/modules/research-framework/portal/src/components/home/StartSessionFromProjectButton.tsx
index bbd734525d..5d5d2be100 100644
---
a/modules/research-framework/portal/src/components/home/StartSessionFromProjectButton.tsx
+++
b/modules/research-framework/portal/src/components/home/StartSessionFromProjectButton.tsx
@@ -14,6 +14,7 @@ import { useState } from "react";
import { Toaster, toaster } from "../ui/toaster";
import { useAuth } from "react-oidc-context";
import { useNavigate } from "react-router";
+import { AxiosError } from "axios";
export const StartSessionFromProjectButton = ({
project,
@@ -67,11 +68,17 @@ export const StartSessionFromProjectButton = ({
type: "success",
});
} catch (error) {
- console.error("Error fetching project:", error);
+ const err = error as AxiosError<unknown>;
+ let msg: string = (err.response?.data as { message: string })?.message;
+
+ if (!msg) {
+ msg =
+ "This is likely because you just made an account and haven't been
enabled yet. Please let us know so we can enable your account";
+ }
+
toaster.create({
title: "Error starting session",
- description:
- "This is likely because you just made an account and haven't been
enabled yet. Please let us know so we can enable your account.",
+ description: msg,
type: "error",
});
}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
index 2834e04d40..ae7b31c809 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
@@ -105,7 +105,7 @@ public class DevDataInitializer implements
CommandLineRunner {
@Override
public void run(String... args) {
- if (projectRepository.existsByOwnerId(devUserEmail)) {
+ if (projectRepository.count() > 0) {
System.out.println("Dev data already initialized. Skipping
initialization.");
return;
}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/GlobalExceptionController.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/GlobalExceptionController.java
index af053d40a4..28c2ddcf6d 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/GlobalExceptionController.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/GlobalExceptionController.java
@@ -26,6 +26,9 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
+import java.util.HashMap;
+import java.util.Map;
+
@ControllerAdvice
public class GlobalExceptionController {
@@ -48,10 +51,13 @@ public class GlobalExceptionController {
}
@ExceptionHandler(Exception.class)
- public ResponseEntity<String> handleOtherExceptions(Exception ex) {
+ public ResponseEntity<Map<String, String>> handleOtherExceptions(Exception
ex) {
LOGGER.error("Unexpected error occurred: ", ex);
+ Map<String, String> errorResponse = new HashMap<>();
+ errorResponse.put("message", ex.getMessage());
+
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
- .body("Unexpected error occurred");
+ .body(errorResponse);
}
-}
\ No newline at end of file
+}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResearchHubHandler.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResearchHubHandler.java
index d5fbc60220..de5484b481 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResearchHubHandler.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResearchHubHandler.java
@@ -18,6 +18,7 @@
*/
package org.apache.airavata.research.service.handlers;
+import org.apache.airavata.research.service.enums.SessionStatusEnum;
import org.apache.airavata.research.service.model.UserContext;
import org.apache.airavata.research.service.model.entity.DatasetResource;
import org.apache.airavata.research.service.model.entity.Project;
@@ -27,8 +28,14 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
@Service
public class ResearchHubHandler {
@@ -37,6 +44,8 @@ public class ResearchHubHandler {
private static final String RH_SPAWN_URL =
"%s/hub/spawn/%s/%s?git=%s&dataPath=%s";
private static final String RH_SESSION_URL = "%s/hub/spawn/%s/%s";
+ private static final String SERVERS_API_URL =
"%s/hub/api/users/%s/servers/%s";
+
private final ProjectHandler projectHandler;
private final SessionHandler sessionHandler;
private final ProjectRepository projectRepository;
@@ -44,13 +53,77 @@ public class ResearchHubHandler {
@Value("${airavata.research-hub.url}")
private String csHubUrl;
+ @Value("${airavata.research-hub.adminApiKey}")
+ private String adminApiKey;
+
+ @Value("${airavata.research-hub.limit}")
+ private int maxRHubSessions;
+
public ResearchHubHandler(ProjectHandler projectHandler, SessionHandler
sessionHandler, ProjectRepository projectRepository) {
this.projectHandler = projectHandler;
this.sessionHandler = sessionHandler;
this.projectRepository = projectRepository;
}
+ public boolean stopSession(String sessionId) {
+ String userId = UserContext.userId();
+ String url = String.format(SERVERS_API_URL, csHubUrl, userId,
sessionId);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("Authorization", "token " + adminApiKey);
+ HttpEntity<Void> request = new HttpEntity<>(headers);
+
+ ResponseEntity<Void> response = new RestTemplate().exchange(
+ url,
+ HttpMethod.DELETE,
+ request,
+ Void.class
+ );
+
+ if (response.getStatusCode().is2xxSuccessful()) {
+ LOGGER.info("Successfully stopped/deleted RHub session {} for user
{}", sessionId, userId);
+ return true;
+ } else {
+ throw new RuntimeException("Failed to delete RHub session " +
sessionId + " for user " + userId);
+ }
+ }
+
+ public boolean deleteSession(String sessionId) {
+ String userId = UserContext.userId();
+ String url = String.format(SERVERS_API_URL, csHubUrl, userId,
sessionId);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("Authorization", "token " + adminApiKey);
+
+ Map<String, Object> body = new HashMap<>();
+ body.put("remove", true);
+
+ HttpEntity<Map<String, Object>> request = new HttpEntity<>(body,
headers);
+
+ ResponseEntity<Void> response = new RestTemplate().exchange(
+ url,
+ HttpMethod.DELETE,
+ request,
+ Void.class
+ );
+
+ if (response.getStatusCode().is2xxSuccessful()) {
+ LOGGER.info("Successfully stopped/deleted RHub session {} for user
{}", sessionId, userId);
+ return true;
+ } else {
+ throw new RuntimeException("Failed to delete RHub session " +
sessionId + " for user " + userId);
+ }
+ }
+
+
+
public String spinRHubSession(String projectId, String sessionName) {
+ String userId = UserContext.userId();
+ int alreadyCreated =
sessionHandler.countSessionsByUserIdAndStatus(userId,
SessionStatusEnum.CREATED);
+ if (alreadyCreated >= maxRHubSessions) {
+ throw new RuntimeException("Max number of active sessions (10) has
already been reached. Please terminate or delete a session to continue.");
+ }
+
Project project = projectHandler.findProject(projectId);
// TODO should support multiple data sets for RHub
DatasetResource dataset =
project.getDatasetResources().stream().findFirst().get();
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/SessionHandler.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/SessionHandler.java
index 6f9e666ceb..75bf7ae3fe 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/SessionHandler.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/SessionHandler.java
@@ -27,6 +27,7 @@ import
org.apache.airavata.research.service.model.repo.SessionRepository;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -39,8 +40,11 @@ public class SessionHandler {
private final SessionRepository sessionRepository;
- public SessionHandler(SessionRepository sessionRepository) {
+ private final ResearchHubHandler researchHubHandler;
+
+ public SessionHandler(SessionRepository sessionRepository, @Lazy
ResearchHubHandler researchHubHandler) {
this.sessionRepository = sessionRepository;
+ this.researchHubHandler = researchHubHandler;
}
public Session findSession(String sessionId) {
@@ -69,23 +73,34 @@ public class SessionHandler {
public Session updateSessionStatus(String sessionId, SessionStatusEnum
status) {
Session session = findSession(sessionId);
+
+ String userId = UserContext.userId();
+ if (!session.getUserId().equals(userId)) {
+ throw new RuntimeException("User is not authorized to update
session");
+ }
+
+ if (status == SessionStatusEnum.TERMINATED) {
+ researchHubHandler.stopSession(sessionId);
+ }
+
session.setStatus(status);
session = sessionRepository.save(session);
LOGGER.debug("Updated session with Id: {}, Status: {}",
session.getId(), status);
return session;
}
- public boolean deleteSession(String sessionId) {
- Session session = findSession(sessionId);
- sessionRepository.delete(session);
- return true;
+ public int countSessionsByUserIdAndStatus(String userId, SessionStatusEnum
status) {
+ return sessionRepository.countSessionsByUserIdAndStatus(userId,
status);
}
- public boolean checkIfSessionExists(String projectId, String userId) {
- if (sessionRepository.findSessionByProjectIdAndUserId(projectId,
userId).isEmpty()) {
- throw new RuntimeException("Session does not exist");
+ public boolean deleteSession(String sessionId) {
+ Session session = findSession(sessionId);
+ if (!session.getUserId().equals(UserContext.userId())) {
+ throw new RuntimeException("Invalid session ID");
}
+
+ researchHubHandler.deleteSession(sessionId);
+ sessionRepository.delete(session);
return true;
}
-
}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/SessionRepository.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/SessionRepository.java
index 15fe1ea08b..2c2dc04a58 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/SessionRepository.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/SessionRepository.java
@@ -38,4 +38,6 @@ public interface SessionRepository extends
JpaRepository<Session, String> {
List<Session> findByUserIdOrderByCreatedAtDesc(String userId);
List<Session> findByUserIdAndStatusOrderByCreatedAtDesc(String userId,
SessionStatusEnum status);
+
+ int countSessionsByUserIdAndStatus(String userId, SessionStatusEnum
status);
}
diff --git
a/modules/research-framework/research-service/src/main/resources/application.yml
b/modules/research-framework/research-service/src/main/resources/application.yml
index ac21eb7ba2..2144268d25 100644
---
a/modules/research-framework/research-service/src/main/resources/application.yml
+++
b/modules/research-framework/research-service/src/main/resources/application.yml
@@ -8,17 +8,20 @@ server:
airavata:
research-hub:
- url: https://hub.dev.cybershuttle.org
+ url: https://hub.cybershuttle.org
dev-user: "[email protected]"
+ adminApiKey: "JUPYTER_ADMIN_API_KEY"
+ limit: 10
+
research-portal:
url: http://localhost:5173
openid:
- url:
"https://auth.dev.cybershuttle.org/realms/default/.well-known/openid-configuration"
+ url:
"https://auth.cybershuttle.org/realms/default/.well-known/openid-configuration"
user-profile:
server:
- url: api.dev.cybershuttle.org
+ url: api.cybershuttle.org
port: 8962
spring:
@@ -51,5 +54,3 @@ springdoc:
use-pkce-with-authorization-code-grant: true
client-id: data-catalog-portal
-
-