| @@ -4,11 +4,11 @@ import com.ruoyi.common.core.web.controller.BaseController; | |||
| import com.ruoyi.common.core.web.domain.AjaxResult; | |||
| import com.ruoyi.common.core.web.domain.GenericsAjaxResult; | |||
| import com.ruoyi.platform.service.JupyterService; | |||
| import com.ruoyi.platform.vo.FrameLogPathVo; | |||
| import io.swagger.annotations.Api; | |||
| import io.swagger.annotations.ApiOperation; | |||
| import org.springframework.web.bind.annotation.GetMapping; | |||
| import org.springframework.web.bind.annotation.RequestMapping; | |||
| import org.springframework.web.bind.annotation.RestController; | |||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | |||
| import org.springframework.web.bind.annotation.*; | |||
| import javax.annotation.Resource; | |||
| import java.io.File; | |||
| @@ -28,6 +28,35 @@ public class JupyterController extends BaseController { | |||
| return genericsSuccess(jupyterService.getJupyterServiceUrl()); | |||
| } | |||
| /** | |||
| * 启动jupyter容器接口 | |||
| * | |||
| * @param id 开发环境配置id | |||
| * @return url | |||
| */ | |||
| @PostMapping("/run/{id}") | |||
| @ApiOperation("根据开发环境id启动jupyter pod") | |||
| @ApiResponse | |||
| public GenericsAjaxResult<String> runJupyter(@PathVariable("id") Integer id) throws Exception { | |||
| return genericsSuccess(this.jupyterService.runJupyterService(id)); | |||
| } | |||
| /** | |||
| * 停止jupyter容器接口 | |||
| * | |||
| * @param id 开发环境配置id | |||
| * @return 操作结果 | |||
| */ | |||
| @DeleteMapping("/stop/{id}") | |||
| @ApiOperation("根据开发环境id停止jupyter pod") | |||
| @ApiResponse | |||
| public GenericsAjaxResult<String> stopJupyter(@PathVariable("id") Integer id) throws Exception { | |||
| return genericsSuccess(this.jupyterService.stopJupyterService(id)); | |||
| } | |||
| @GetMapping(value = "/upload") | |||
| public AjaxResult upload() throws Exception { | |||
| File file = new File("D://nexus-deploy.yaml"); | |||
| @@ -1,5 +1,7 @@ | |||
| package com.ruoyi.platform.service; | |||
| import com.ruoyi.platform.vo.FrameLogPathVo; | |||
| import java.io.InputStream; | |||
| public interface JupyterService { | |||
| @@ -8,4 +10,8 @@ public interface JupyterService { | |||
| void upload(InputStream inputStream); | |||
| void mlflow(); | |||
| String runJupyterService(Integer id); | |||
| String stopJupyterService(Integer id) throws Exception; | |||
| } | |||
| @@ -4,7 +4,10 @@ import com.ruoyi.common.security.utils.SecurityUtils; | |||
| import com.ruoyi.platform.domain.DevEnvironment; | |||
| import com.ruoyi.platform.mapper.DevEnvironmentDao; | |||
| import com.ruoyi.platform.service.DevEnvironmentService; | |||
| import com.ruoyi.platform.service.JupyterService; | |||
| import com.ruoyi.platform.utils.JacksonUtil; | |||
| import com.ruoyi.system.api.model.LoginUser; | |||
| import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; | |||
| import org.apache.commons.lang3.StringUtils; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.data.domain.Page; | |||
| @@ -13,6 +16,7 @@ import org.springframework.data.domain.PageRequest; | |||
| import javax.annotation.Resource; | |||
| import java.util.Date; | |||
| import java.util.Map; | |||
| /** | |||
| * (DevEnvironment)表服务实现类 | |||
| @@ -25,6 +29,11 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { | |||
| @Resource | |||
| private DevEnvironmentDao devEnvironmentDao; | |||
| @Resource | |||
| private JupyterService jupyterService; | |||
| /** | |||
| * 通过ID查询单条数据 | |||
| * | |||
| @@ -111,4 +120,6 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { | |||
| devEnvironment.setState(0); | |||
| return this.devEnvironmentDao.update(devEnvironment)>0?"删除成功":"删除失败"; | |||
| } | |||
| } | |||
| @@ -1,7 +1,12 @@ | |||
| package com.ruoyi.platform.service.impl; | |||
| import com.ruoyi.common.redis.service.RedisService; | |||
| import com.ruoyi.common.security.utils.SecurityUtils; | |||
| import com.ruoyi.platform.domain.DevEnvironment; | |||
| import com.ruoyi.platform.mapper.DevEnvironmentDao; | |||
| import com.ruoyi.platform.service.DevEnvironmentService; | |||
| import com.ruoyi.platform.service.JupyterService; | |||
| import com.ruoyi.platform.utils.JacksonUtil; | |||
| import com.ruoyi.platform.utils.K8sClientUtil; | |||
| import com.ruoyi.platform.utils.MinioUtil; | |||
| import com.ruoyi.platform.utils.MlflowUtil; | |||
| @@ -13,6 +18,7 @@ import org.springframework.stereotype.Service; | |||
| import javax.annotation.Resource; | |||
| import java.io.InputStream; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| @Service | |||
| public class JupyterServiceImpl implements JupyterService { | |||
| @@ -39,6 +45,15 @@ public class JupyterServiceImpl implements JupyterService { | |||
| @Resource | |||
| private MlflowUtil mlflowUtil; | |||
| @Resource | |||
| private DevEnvironmentDao devEnvironmentDao; | |||
| @Resource | |||
| private DevEnvironmentService devEnvironmentService; | |||
| @Resource | |||
| private RedisService redisService; | |||
| public JupyterServiceImpl(MinioUtil minioUtil) { | |||
| this.minioUtil = minioUtil; | |||
| } | |||
| @@ -53,6 +68,54 @@ public class JupyterServiceImpl implements JupyterService { | |||
| return masterIp + ":" + podPort; | |||
| } | |||
| @Override | |||
| public String runJupyterService(Integer id) { | |||
| DevEnvironment devEnvironment = this.devEnvironmentDao.queryById(id); | |||
| String envName = devEnvironment.getName(); | |||
| //TODO 设置环境变量 | |||
| // 提取数据集,模型信息,得到数据集模型的path | |||
| Map<String, Object> dataset = JacksonUtil.parseJSONStr2Map(devEnvironment.getDataset()); | |||
| String datasetPath = (String) dataset.get("path"); | |||
| Map<String, Object> model = JacksonUtil.parseJSONStr2Map(devEnvironment.getModel()); | |||
| String modelPath = (String) model.get("path"); | |||
| LoginUser loginUser = SecurityUtils.getLoginUser(); | |||
| String podName = loginUser.getUsername().toLowerCase() + "-editor-pod"; | |||
| String pvcName = loginUser.getUsername().toLowerCase() + "-editor-pvc"; | |||
| V1PersistentVolumeClaim pvc = k8sClientUtil.createPvc(namespace, pvcName, storage, storageClassName); | |||
| //TODO 设置镜像可配置,这里先用默认镜像启动pod | |||
| // 调用修改后的 createPod 方法,传入额外的参数 | |||
| Integer podPort = k8sClientUtil.createConfiguredPod(podName, namespace, port, mountPath, pvc, image, datasetPath, modelPath); | |||
| return masterIp + ":" + podPort; | |||
| } | |||
| @Override | |||
| public String stopJupyterService(Integer id) throws Exception { | |||
| DevEnvironment devEnvironment = this.devEnvironmentDao.queryById(id); | |||
| if (devEnvironment==null){ | |||
| throw new Exception("开发环境配置不存在"); | |||
| } | |||
| LoginUser loginUser = SecurityUtils.getLoginUser(); | |||
| String podName = loginUser.getUsername().toLowerCase() + "-editor-pod"; | |||
| // 使用 Kubernetes API 删除 Pod | |||
| String deleteResult = k8sClientUtil.deletePod(podName, namespace); | |||
| // 检查 Pod 是否存在 | |||
| boolean exists = k8sClientUtil.checkPodExists(podName, namespace); | |||
| if (exists) { | |||
| throw new Exception("Pod " + podName + " 删除失败"); | |||
| } | |||
| return deleteResult + ",编辑器已停止"; | |||
| } | |||
| @Override | |||
| public void upload(InputStream inputStream) { | |||
| try { | |||
| @@ -71,4 +134,6 @@ public class JupyterServiceImpl implements JupyterService { | |||
| } | |||
| } | |||
| @@ -22,9 +22,7 @@ import org.springframework.stereotype.Component; | |||
| import javax.annotation.PostConstruct; | |||
| import java.io.BufferedReader; | |||
| import java.io.InputStreamReader; | |||
| import java.util.HashMap; | |||
| import java.util.LinkedHashMap; | |||
| import java.util.Map; | |||
| import java.util.*; | |||
| /** | |||
| * k8s客户端 | |||
| @@ -282,13 +280,13 @@ public class K8sClientUtil { | |||
| .endSpec() | |||
| .build(); | |||
| try { | |||
| pod = api.createNamespacedPod(namespace, pod, null, null, null); | |||
| } catch (ApiException e) { | |||
| log.error("创建pod异常:" + e.getResponseBody(), e); | |||
| } catch (Exception e) { | |||
| log.error("创建pod系统异常:", e); | |||
| } | |||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | |||
| @@ -324,7 +322,6 @@ public class K8sClientUtil { | |||
| for (V1Pod pod1 : v1PodList.getItems()) { | |||
| if (StringUtils.equals(pod1.getMetadata().getName(), podName)) { | |||
| // PVC 已存在 | |||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | |||
| if (service != null) { | |||
| return service.getSpec().getPorts().get(0).getNodePort(); | |||
| @@ -378,6 +375,73 @@ public class K8sClientUtil { | |||
| return service.getSpec().getPorts().get(0).getNodePort(); | |||
| } | |||
| public Integer createConfiguredPod(String podName, String namespace, Integer port, String mountPath, V1PersistentVolumeClaim pvc, String image, String datasetPath, String modelPath) { | |||
| Map<String, String> selector = new LinkedHashMap<>(); | |||
| selector.put("k8s-jupyter", podName); | |||
| CoreV1Api api = new CoreV1Api(apiClient); | |||
| V1PodList v1PodList = null; | |||
| try { | |||
| v1PodList = api.listNamespacedPod(namespace, null, null, null, null, null, null, null, null, null, null); | |||
| } catch (ApiException e) { | |||
| log.error("获取 POD 异常:", e); | |||
| } | |||
| if (v1PodList != null) { | |||
| for (V1Pod pod1 : v1PodList.getItems()) { | |||
| // PVC 已存在 | |||
| if (StringUtils.equals(pod1.getMetadata().getName(), podName)) { | |||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | |||
| if (service != null) { | |||
| return service.getSpec().getPorts().get(0).getNodePort(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // 配置卷和卷挂载 | |||
| List<V1VolumeMount> volumeMounts = new ArrayList<>(); | |||
| volumeMounts.add(new V1VolumeMount().name("workspace").mountPath(mountPath)); | |||
| volumeMounts.add(new V1VolumeMount().name("dataset").mountPath("/datasets").subPath(datasetPath).readOnly(true)); | |||
| volumeMounts.add(new V1VolumeMount().name("model").mountPath("/model").subPath(modelPath).readOnly(true)); | |||
| List<V1Volume> volumes = new ArrayList<>(); | |||
| volumes.add(new V1Volume().name("workspace").persistentVolumeClaim(new V1PersistentVolumeClaimVolumeSource().claimName(pvc.getMetadata().getName()))); | |||
| volumes.add(new V1Volume().name("dataset").persistentVolumeClaim(new V1PersistentVolumeClaimVolumeSource().claimName(pvc.getMetadata().getName()))); | |||
| volumes.add(new V1Volume().name("model").persistentVolumeClaim(new V1PersistentVolumeClaimVolumeSource().claimName(pvc.getMetadata().getName()))); | |||
| V1Pod pod = new V1PodBuilder() | |||
| .withNewMetadata() | |||
| .withName(podName) | |||
| .withLabels(selector) | |||
| .endMetadata() | |||
| .withNewSpec() | |||
| .addNewContainer() | |||
| .withName(podName) | |||
| .withImage(image) | |||
| .withPorts(new V1ContainerPort().containerPort(port).protocol("TCP")) | |||
| .withVolumeMounts(volumeMounts) | |||
| .endContainer() | |||
| .withVolumes(volumes) | |||
| .withTerminationGracePeriodSeconds(14400L) | |||
| .endSpec() | |||
| .build(); | |||
| try { | |||
| pod = api.createNamespacedPod(namespace, pod, null, null, null); | |||
| } catch (ApiException e) { | |||
| log.error("创建pod异常:" + e.getResponseBody(), e); | |||
| } catch (Exception e) { | |||
| log.error("创建pod系统异常:", e); | |||
| } | |||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | |||
| return service.getSpec().getPorts().get(0).getNodePort(); | |||
| } | |||
| /** | |||
| * 根据获取namespace,deploymentName的Pod Name | |||
| * | |||
| @@ -495,4 +559,51 @@ public class K8sClientUtil { | |||
| } | |||
| return pod; | |||
| } | |||
| /** | |||
| * 删除 Pod | |||
| * | |||
| * @param podName Pod 名称 | |||
| * @param namespace 命名空间 | |||
| * @throws ApiException 异常 | |||
| */ | |||
| public String deletePod(String podName, String namespace) throws ApiException { | |||
| CoreV1Api api = new CoreV1Api(apiClient); | |||
| try { | |||
| V1Pod pod = api.deleteNamespacedPod(podName, namespace, null, null, null, null, null, null); | |||
| return "Pod " + podName + " 删除请求已发送"; | |||
| } catch (ApiException e) { | |||
| if (e.getCode() == 404) { | |||
| return "Pod " + podName + " 不存在"; | |||
| } else { | |||
| log.error("删除pod异常:" + e.getResponseBody(), e); | |||
| throw e; | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * 检查 Pod 是否存在 | |||
| * | |||
| * @param podName Pod 名称 | |||
| * @param namespace 命名空间 | |||
| * @return 是否存在 | |||
| * @throws ApiException 异常 | |||
| */ | |||
| public boolean checkPodExists(String podName, String namespace) throws ApiException { | |||
| CoreV1Api api = new CoreV1Api(apiClient); | |||
| try { | |||
| api.readNamespacedPod(podName, namespace, null,false,false); | |||
| return true; | |||
| } catch (ApiException e) { | |||
| if (e.getCode() == 404) { | |||
| return false; | |||
| } else { | |||
| log.error("检查pod存在性异常:" + e.getResponseBody(), e); | |||
| throw e; | |||
| } | |||
| } | |||
| } | |||
| } | |||