-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
474 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -269,6 +269,17 @@ public class ProjectManagerConst { | |
public final static String TOKEN_MANAGER_PARAMETER_TOKEN_STATUS = "token_status"; | ||
public final static String TOKEN_MANAGER_PARAMETER_TOKEN_CREATED_AT = "token_created_at"; | ||
|
||
// Coder | ||
public final static String CODER_API_PATH = "/api/v2"; | ||
public final static String CODER_SESSION_TOKEN_HEADER = "Coder-Session-Token"; | ||
|
||
public final static String CODER_ENABLE_JUPYTER_LAB_PARAM_KEY = "Enable Jupyter Lab?"; | ||
public final static String CODER_ENABLE_VS_CODE_SERVER_PARAM_KEY = "Enable VS Code Server?"; | ||
public final static String CODER_DOTFILES_URL_PARAM_KEY = "Your Dotfiles URL"; | ||
public final static String CODER_ENABLE_FILE_RECEIVER_PARAM_KEY = "Samply.Beam: Enable file receiver"; | ||
public final static String CODER_SAMPLY_BEAM_APP_ID_PARAM_KEY = "Samply.Beam: App ID (short)"; | ||
public final static String CODER_SAMPLY_BEAM_APP_SECRET_PARAM_KEY = "Samply.Beam: App Secret"; | ||
public final static String CODER_DELETE_TRANSITION = "delete"; | ||
|
||
// Environment Variables | ||
public final static String PM_ADMIN_GROUPS = "PM_ADMIN_GROUPS"; | ||
|
@@ -319,6 +330,21 @@ public class ProjectManagerConst { | |
public final static String ENABLE_EXPORTER = "ENABLE_EXPORTER"; | ||
public final static String MAX_TIME_TO_WAIT_FOCUS_TASK_IN_MINUTES = "MAX_TIME_TO_WAIT_FOCUS_TASK_IN_MINUTES"; | ||
|
||
public final static String CODER_BASE_URL = "CODER_BASE_URL"; | ||
public final static String CODER_ORGANISATION_ID = "CODER_ORGANISATION_ID"; | ||
public final static String CODER_MEMBER_ID = "CODER_MEMBER_ID"; | ||
public final static String CODER_WORKSPACE_ID = "CODER_WORKSPACE_ID"; | ||
public final static String CODER_TEMPLATE_VERSION_ID = "CODER_TEMPLATE_VERSION_ID"; | ||
public final static String CODER_CREATE_PATH = "CODER_CREATE_PATH"; | ||
public final static String CODER_DELETE_PATH = "CODER_DELETE_PATH"; | ||
public final static String CODER_SESSION_TOKEN = "CODER_SESSION_TOKEN"; | ||
|
||
public final static String CODER_ENABLE_JUPYTER_LAB_PARAM_VALUE = "CODER_ENABLE_JUPYTER_LAB_PARAM_VALUE"; | ||
public final static String CODER_ENABLE_VS_CODE_SERVER_PARAM_VALUE = "CODER_ENABLE_VS_CODE_SERVER_PARAM_VALUE"; | ||
public final static String CODER_DOTFILES_URL_PARAM_VALUE = "CODER_DOTFILES_URL_PARAM_VALUE"; | ||
public final static String CODER_ENABLE_FILE_RECEIVER_PARAM_VALUE = "CODER_ENABLE_FILE_RECEIVER_PARAM_VALUE"; | ||
public final static String ENABLE_CODER = "ENABLE_CODER"; | ||
|
||
// Spring Values (SV) | ||
public final static String HEAD_SV = "${"; | ||
public final static String BOTTOM_SV = "}"; | ||
|
@@ -387,7 +413,19 @@ public class ProjectManagerConst { | |
public final static String ENABLE_RSTUDIO_GROUP_MANAGER_SV = HEAD_SV + ENABLE_RSTUDIO_GROUP_MANAGER + ":true" + BOTTOM_SV; | ||
public final static String OIDC_URL_SV = HEAD_SV + OIDC_URL + BOTTOM_SV; | ||
public final static String OIDC_REALM_SV = HEAD_SV + OIDC_REALM + BOTTOM_SV; | ||
public final static String CODER_BASE_URL_SV = HEAD_SV + CODER_BASE_URL + BOTTOM_SV; | ||
public final static String CODER_ORGANISATION_ID_SV = HEAD_SV + CODER_ORGANISATION_ID + BOTTOM_SV; | ||
public final static String CODER_MEMBER_ID_SV = HEAD_SV + CODER_MEMBER_ID + BOTTOM_SV; | ||
public final static String CODER_TEMPLATE_VERSION_ID_SV = HEAD_SV + CODER_TEMPLATE_VERSION_ID + BOTTOM_SV; | ||
public final static String CODER_CREATE_PATH_SV = HEAD_SV + CODER_CREATE_PATH + BOTTOM_SV; | ||
public final static String CODER_DELETE_PATH_SV = HEAD_SV + CODER_DELETE_PATH + BOTTOM_SV; | ||
public final static String CODER_SESSION_TOKEN_SV = HEAD_SV + CODER_SESSION_TOKEN + BOTTOM_SV; | ||
|
||
public final static String CODER_ENABLE_JUPYTER_LAB_PARAM_VALUE_SV = HEAD_SV + CODER_ENABLE_JUPYTER_LAB_PARAM_VALUE + ":1" + BOTTOM_SV; | ||
public final static String CODER_ENABLE_VS_CODE_SERVER_PARAM_VALUE_SV = HEAD_SV + CODER_ENABLE_VS_CODE_SERVER_PARAM_VALUE + ":0" + BOTTOM_SV; | ||
public final static String CODER_DOTFILES_URL_PARAM_VALUE_SV = HEAD_SV + CODER_DOTFILES_URL_PARAM_VALUE + ":" + BOTTOM_SV; | ||
public final static String CODER_ENABLE_FILE_RECEIVER_PARAM_VALUE_SV = HEAD_SV + CODER_ENABLE_FILE_RECEIVER_PARAM_VALUE + ":1" + BOTTOM_SV; | ||
public final static String ENABLE_CODER_SV = HEAD_SV + ENABLE_CODER + ":true" + BOTTOM_SV; | ||
|
||
// Others | ||
public final static String TEST_EMAIL = "[email protected]"; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
package de.samply.coder; | ||
|
||
import de.samply.app.ProjectManagerConst; | ||
import de.samply.coder.request.CreateRequestBody; | ||
import de.samply.coder.request.CreateRequestParameter; | ||
import de.samply.coder.request.Response; | ||
import de.samply.coder.request.TransitionRequestBody; | ||
import de.samply.db.model.Project; | ||
import de.samply.db.model.ProjectCoder; | ||
import de.samply.db.repository.ProjectCoderRepository; | ||
import de.samply.db.repository.ProjectRepository; | ||
import de.samply.notification.NotificationService; | ||
import de.samply.notification.OperationType; | ||
import de.samply.utils.WebClientFactory; | ||
import jakarta.validation.constraints.NotNull; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.web.reactive.function.client.WebClient; | ||
import reactor.core.publisher.Mono; | ||
|
||
import java.time.Instant; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.UUID; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
|
||
@Slf4j | ||
@Service | ||
public class CoderService { | ||
|
||
private final boolean coderEnabled; | ||
private final ProjectRepository projectRepository; | ||
private final ProjectCoderRepository projectCoderRepository; | ||
private final NotificationService notificationService; | ||
private final String coderTemplateVersionId; | ||
private final String coderCreatePath; | ||
private final String coderDeletePath; | ||
private final String enableJupyterLab; | ||
private final String enableVsCodeServer; | ||
private final String dotFilesUrl; | ||
private final String enableFileReceiver; | ||
private final String coderSessionToken; | ||
|
||
private final WebClient webClient; | ||
|
||
public CoderService( | ||
ProjectCoderRepository projectCoderRepository, | ||
NotificationService notificationService, | ||
ProjectRepository projectRepository, | ||
@Value(ProjectManagerConst.ENABLE_CODER_SV) boolean coderEnabled, | ||
@Value(ProjectManagerConst.CODER_BASE_URL_SV) String coderBaseUrl, | ||
@Value(ProjectManagerConst.CODER_ORGANISATION_ID_SV) String coderOrganizationId, | ||
@Value(ProjectManagerConst.CODER_MEMBER_ID_SV) String coderMemberId, | ||
@Value(ProjectManagerConst.CODER_TEMPLATE_VERSION_ID_SV) String coderTemplateVersionId, | ||
@Value(ProjectManagerConst.CODER_CREATE_PATH_SV) String coderCreatePath, | ||
@Value(ProjectManagerConst.CODER_DELETE_PATH_SV) String coderDeletePath, | ||
@Value(ProjectManagerConst.CODER_ENABLE_JUPYTER_LAB_PARAM_VALUE_SV) String enableJupyterLab, | ||
@Value(ProjectManagerConst.CODER_ENABLE_VS_CODE_SERVER_PARAM_VALUE_SV) String enableVsCodeServer, | ||
@Value(ProjectManagerConst.CODER_DOTFILES_URL_PARAM_VALUE_SV) String dotFilesUrl, | ||
@Value(ProjectManagerConst.CODER_ENABLE_FILE_RECEIVER_PARAM_VALUE_SV) String enableFileReceiver, | ||
@Value(ProjectManagerConst.CODER_SESSION_TOKEN_SV) String coderSessionToken, | ||
WebClientFactory webClientFactory) { | ||
this.coderEnabled = coderEnabled; | ||
this.projectCoderRepository = projectCoderRepository; | ||
this.notificationService = notificationService; | ||
this.projectRepository = projectRepository; | ||
this.enableJupyterLab = enableJupyterLab; | ||
this.enableVsCodeServer = enableVsCodeServer; | ||
this.dotFilesUrl = dotFilesUrl; | ||
this.enableFileReceiver = enableFileReceiver; | ||
this.coderSessionToken = coderSessionToken; | ||
Map<String, String> pathVariables = Map.of(ProjectManagerConst.CODER_ORGANISATION_ID, coderOrganizationId, | ||
ProjectManagerConst.CODER_MEMBER_ID, coderMemberId); | ||
this.coderCreatePath = replaceVariablesInPath(coderCreatePath, pathVariables); | ||
this.coderDeletePath = replaceVariablesInPath(coderDeletePath, pathVariables); | ||
|
||
this.coderTemplateVersionId = coderTemplateVersionId; | ||
this.webClient = webClientFactory.createWebClient(coderBaseUrl); | ||
} | ||
|
||
private String replaceVariablesInPath(String path, Map<String, String> pathVariables) { | ||
AtomicReference<String> result = new AtomicReference<>(path); | ||
if (path != null) { | ||
pathVariables.entrySet().stream().filter(entry -> entry.getKey() != null && entry.getValue() != null) | ||
.forEach(entry -> result.set(result.get().replace(fetchVariableExpresion(entry.getKey()), entry.getValue()))); | ||
} | ||
|
||
return result.get(); | ||
} | ||
|
||
private String fetchVariableExpresion(String variable) { | ||
return "{" + variable + "}"; | ||
} | ||
|
||
public void createWorkspace(String email, String projectCode) throws CoderServiceException { | ||
Optional<Project> project = projectRepository.findByCode(projectCode); | ||
if (project.isEmpty()) { | ||
throw new CoderServiceException("Project " + projectCode + " not found"); | ||
} | ||
createWorkspace(email, project.get()); | ||
} | ||
|
||
public void createWorkspace(@NotNull String email, @NotNull Project project) { | ||
if (coderEnabled) { | ||
ProjectCoder projectCoder = generateProjectCoder(email, project); | ||
CreateRequestBody createRequestBody = generateCreateRequestBody(projectCoder); | ||
Response response = createWorkspace(projectCoder, createRequestBody).block(); | ||
projectCoder.setWorkspaceId(response.getLatestBuild().getWorkspaceId()); | ||
projectCoderRepository.save(projectCoder); | ||
notificationService.createNotification(project.getCode(), null, email, OperationType.CREATE_CODER_WORKSPACE, | ||
"Created workspace " + projectCoder.getWorkspaceId(), null, null); | ||
|
||
} | ||
} | ||
|
||
private Mono<Response> createWorkspace(ProjectCoder projectCoder, CreateRequestBody createRequestBody) { | ||
return this.webClient.post() | ||
.uri(uriBuilder -> uriBuilder.path(ProjectManagerConst.CODER_API_PATH).path(coderCreatePath).build()) | ||
.header(ProjectManagerConst.CODER_SESSION_TOKEN_HEADER, coderSessionToken) | ||
.contentType(MediaType.APPLICATION_JSON) | ||
.bodyValue(createRequestBody) | ||
.exchangeToMono(clientResponse -> { | ||
if (clientResponse.statusCode().equals(HttpStatus.OK) || clientResponse.statusCode().equals(HttpStatus.CREATED)) { | ||
return clientResponse.bodyToMono(Response.class); | ||
} else { | ||
log.error("Http error " + clientResponse.statusCode() + " creating workspace in Coder for user " | ||
+ projectCoder.getEmail() + " in project " + projectCoder.getProject().getCode()); | ||
return clientResponse.bodyToMono(String.class).flatMap(errorBody -> { | ||
log.error("Error: {}", errorBody); | ||
return Mono.error(new RuntimeException(errorBody)); | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
private ProjectCoder generateProjectCoder(String email, Project project) { | ||
ProjectCoder projectCoder = new ProjectCoder(); | ||
projectCoder.setProject(project); | ||
projectCoder.setEmail(email); | ||
projectCoder.setAppId(fetchCoderAppId(email, project)); | ||
projectCoder.setAppSecret(generateAppSecret()); | ||
return projectCoder; | ||
} | ||
|
||
public void deleteWorkspace(@NotNull String email, @NotNull String projectCode) throws CoderServiceException { | ||
Optional<Project> project = projectRepository.findByCode(projectCode); | ||
if (project.isEmpty()) { | ||
throw new CoderServiceException("Project " + projectCode + " not found"); | ||
} | ||
deleteWorkspace(email, project.get()); | ||
} | ||
|
||
public void deleteWorkspace(@NotNull String email, @NotNull Project project) { | ||
if (coderEnabled) { | ||
projectCoderRepository.findByProjectAndEmail(project, email) | ||
.filter(projectCoder -> projectCoder.getDeletedAt() == null).ifPresent(projectCoder -> { | ||
deleteWorkspace(projectCoder).block(); | ||
projectCoder.setDeletedAt(Instant.now()); | ||
projectCoderRepository.save(projectCoder); | ||
notificationService.createNotification(project.getCode(), null, email, OperationType.DELETE_CODER_WORKSPACE, | ||
"Deleted workspace " + projectCoder.getWorkspaceId(), null, null); | ||
}); | ||
} | ||
} | ||
|
||
private Mono<Response> deleteWorkspace(ProjectCoder projectCoder) { | ||
return this.webClient.post() | ||
.uri(uriBuilder -> uriBuilder.path(ProjectManagerConst.CODER_API_PATH) | ||
.path(replaceVariablesInPath(coderDeletePath, Map.of(ProjectManagerConst.CODER_WORKSPACE_ID, projectCoder.getWorkspaceId()))).build()) | ||
.header(ProjectManagerConst.CODER_SESSION_TOKEN_HEADER, coderSessionToken) | ||
.contentType(MediaType.APPLICATION_JSON) | ||
.bodyValue(new TransitionRequestBody(ProjectManagerConst.CODER_DELETE_TRANSITION)) | ||
.exchangeToMono(clientResponse -> { | ||
if (clientResponse.statusCode().equals(HttpStatus.OK) || clientResponse.statusCode().equals(HttpStatus.CREATED)) { | ||
return clientResponse.bodyToMono(Response.class); | ||
} else { | ||
log.error("Http error " + clientResponse.statusCode() + " deleting workspace in Coder for user " | ||
+ projectCoder.getEmail() + " in project " + projectCoder.getProject().getCode()); | ||
return clientResponse.bodyToMono(String.class).flatMap(errorBody -> { | ||
log.error("Error: {}", errorBody); | ||
return Mono.error(new RuntimeException(errorBody)); | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
private CreateRequestBody generateCreateRequestBody(ProjectCoder projectCoder) { | ||
CreateRequestBody createRequestBody = new CreateRequestBody(); | ||
createRequestBody.setTemplateVersionId(coderTemplateVersionId); | ||
createRequestBody.setName(projectCoder.getAppId()); | ||
addRichParameterValues(createRequestBody, projectCoder); | ||
return createRequestBody; | ||
} | ||
|
||
private void addRichParameterValues(CreateRequestBody createRequestBody, ProjectCoder projectCoder) { | ||
List<CreateRequestParameter> createRequestParameters = List.of( | ||
new CreateRequestParameter(ProjectManagerConst.CODER_ENABLE_JUPYTER_LAB_PARAM_KEY, enableJupyterLab), | ||
new CreateRequestParameter(ProjectManagerConst.CODER_ENABLE_VS_CODE_SERVER_PARAM_KEY, enableVsCodeServer), | ||
new CreateRequestParameter(ProjectManagerConst.CODER_DOTFILES_URL_PARAM_KEY, dotFilesUrl), | ||
new CreateRequestParameter(ProjectManagerConst.CODER_ENABLE_FILE_RECEIVER_PARAM_KEY, enableFileReceiver), | ||
new CreateRequestParameter(ProjectManagerConst.CODER_SAMPLY_BEAM_APP_ID_PARAM_KEY, projectCoder.getAppId()), | ||
new CreateRequestParameter(ProjectManagerConst.CODER_SAMPLY_BEAM_APP_SECRET_PARAM_KEY, projectCoder.getAppSecret()) | ||
); | ||
createRequestBody.setRichParameterValues(createRequestParameters.toArray(CreateRequestParameter[]::new)); | ||
} | ||
|
||
public String fetchCoderAppId(@NotNull String email, @NotNull Project project) { | ||
return email.substring(0, email.indexOf("@")).replace(".", "-") + "-" + project.getCode(); | ||
} | ||
|
||
private String generateAppSecret() { | ||
return UUID.randomUUID().toString(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package de.samply.coder; | ||
|
||
public class CoderServiceException extends RuntimeException{ | ||
public CoderServiceException(String message) { | ||
super(message); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package de.samply.coder.request; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Data; | ||
|
||
@Data | ||
public class Build { | ||
|
||
@JsonProperty("created_at") | ||
private String createdAt; | ||
|
||
@JsonProperty("workspace_id") | ||
private String workspaceId; | ||
|
||
} |
18 changes: 18 additions & 0 deletions
18
src/main/java/de/samply/coder/request/CreateRequestBody.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package de.samply.coder.request; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Data; | ||
|
||
@Data | ||
public class CreateRequestBody { | ||
|
||
@JsonProperty("name") | ||
private String name; | ||
|
||
@JsonProperty("rich_parameter_values") | ||
private CreateRequestParameter[] richParameterValues; | ||
|
||
@JsonProperty("template_version_id") | ||
private String templateVersionId; | ||
|
||
} |
23 changes: 23 additions & 0 deletions
23
src/main/java/de/samply/coder/request/CreateRequestParameter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package de.samply.coder.request; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Data; | ||
|
||
@Data | ||
public class CreateRequestParameter { | ||
|
||
@JsonProperty("name") | ||
private String name; | ||
|
||
@JsonProperty("value") | ||
private String value; | ||
|
||
public CreateRequestParameter() { | ||
} | ||
|
||
public CreateRequestParameter(String name, String value) { | ||
this.name = name; | ||
this.value = value; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package de.samply.coder.request; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Data; | ||
|
||
@Data | ||
public class Response { | ||
|
||
@JsonProperty("created_at") | ||
private String createdAt; | ||
|
||
@JsonProperty("id") | ||
private String id; | ||
|
||
@JsonProperty("latest_build") | ||
private Build latestBuild; | ||
|
||
@JsonProperty("status") | ||
private String status; | ||
|
||
|
||
} |
Oops, something went wrong.