| @@ -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.AjaxResult; | ||||
| import com.ruoyi.common.core.web.domain.GenericsAjaxResult; | import com.ruoyi.common.core.web.domain.GenericsAjaxResult; | ||||
| import com.ruoyi.platform.service.JupyterService; | import com.ruoyi.platform.service.JupyterService; | ||||
| import com.ruoyi.platform.vo.FrameLogPathVo; | |||||
| import io.swagger.annotations.Api; | import io.swagger.annotations.Api; | ||||
| import io.swagger.annotations.ApiOperation; | 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 javax.annotation.Resource; | ||||
| import java.io.File; | import java.io.File; | ||||
| @@ -28,6 +28,35 @@ public class JupyterController extends BaseController { | |||||
| return genericsSuccess(jupyterService.getJupyterServiceUrl()); | 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") | @GetMapping(value = "/upload") | ||||
| public AjaxResult upload() throws Exception { | public AjaxResult upload() throws Exception { | ||||
| File file = new File("D://nexus-deploy.yaml"); | File file = new File("D://nexus-deploy.yaml"); | ||||
| @@ -1,5 +1,7 @@ | |||||
| package com.ruoyi.platform.service; | package com.ruoyi.platform.service; | ||||
| import com.ruoyi.platform.vo.FrameLogPathVo; | |||||
| import java.io.InputStream; | import java.io.InputStream; | ||||
| public interface JupyterService { | public interface JupyterService { | ||||
| @@ -8,4 +10,8 @@ public interface JupyterService { | |||||
| void upload(InputStream inputStream); | void upload(InputStream inputStream); | ||||
| void mlflow(); | 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.domain.DevEnvironment; | ||||
| import com.ruoyi.platform.mapper.DevEnvironmentDao; | import com.ruoyi.platform.mapper.DevEnvironmentDao; | ||||
| import com.ruoyi.platform.service.DevEnvironmentService; | 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 com.ruoyi.system.api.model.LoginUser; | ||||
| import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; | |||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||
| import org.springframework.data.domain.Page; | import org.springframework.data.domain.Page; | ||||
| @@ -13,6 +16,7 @@ import org.springframework.data.domain.PageRequest; | |||||
| import javax.annotation.Resource; | import javax.annotation.Resource; | ||||
| import java.util.Date; | import java.util.Date; | ||||
| import java.util.Map; | |||||
| /** | /** | ||||
| * (DevEnvironment)表服务实现类 | * (DevEnvironment)表服务实现类 | ||||
| @@ -25,6 +29,11 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { | |||||
| @Resource | @Resource | ||||
| private DevEnvironmentDao devEnvironmentDao; | private DevEnvironmentDao devEnvironmentDao; | ||||
| @Resource | |||||
| private JupyterService jupyterService; | |||||
| /** | /** | ||||
| * 通过ID查询单条数据 | * 通过ID查询单条数据 | ||||
| * | * | ||||
| @@ -111,4 +120,6 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { | |||||
| devEnvironment.setState(0); | devEnvironment.setState(0); | ||||
| return this.devEnvironmentDao.update(devEnvironment)>0?"删除成功":"删除失败"; | return this.devEnvironmentDao.update(devEnvironment)>0?"删除成功":"删除失败"; | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,7 +1,12 @@ | |||||
| package com.ruoyi.platform.service.impl; | package com.ruoyi.platform.service.impl; | ||||
| import com.ruoyi.common.redis.service.RedisService; | |||||
| import com.ruoyi.common.security.utils.SecurityUtils; | 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.service.JupyterService; | ||||
| import com.ruoyi.platform.utils.JacksonUtil; | |||||
| import com.ruoyi.platform.utils.K8sClientUtil; | import com.ruoyi.platform.utils.K8sClientUtil; | ||||
| import com.ruoyi.platform.utils.MinioUtil; | import com.ruoyi.platform.utils.MinioUtil; | ||||
| import com.ruoyi.platform.utils.MlflowUtil; | import com.ruoyi.platform.utils.MlflowUtil; | ||||
| @@ -13,6 +18,7 @@ import org.springframework.stereotype.Service; | |||||
| import javax.annotation.Resource; | import javax.annotation.Resource; | ||||
| import java.io.InputStream; | import java.io.InputStream; | ||||
| import java.util.List; | import java.util.List; | ||||
| import java.util.Map; | |||||
| @Service | @Service | ||||
| public class JupyterServiceImpl implements JupyterService { | public class JupyterServiceImpl implements JupyterService { | ||||
| @@ -39,6 +45,15 @@ public class JupyterServiceImpl implements JupyterService { | |||||
| @Resource | @Resource | ||||
| private MlflowUtil mlflowUtil; | private MlflowUtil mlflowUtil; | ||||
| @Resource | |||||
| private DevEnvironmentDao devEnvironmentDao; | |||||
| @Resource | |||||
| private DevEnvironmentService devEnvironmentService; | |||||
| @Resource | |||||
| private RedisService redisService; | |||||
| public JupyterServiceImpl(MinioUtil minioUtil) { | public JupyterServiceImpl(MinioUtil minioUtil) { | ||||
| this.minioUtil = minioUtil; | this.minioUtil = minioUtil; | ||||
| } | } | ||||
| @@ -53,6 +68,54 @@ public class JupyterServiceImpl implements JupyterService { | |||||
| return masterIp + ":" + podPort; | 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 | @Override | ||||
| public void upload(InputStream inputStream) { | public void upload(InputStream inputStream) { | ||||
| try { | try { | ||||
| @@ -71,4 +134,6 @@ public class JupyterServiceImpl implements JupyterService { | |||||
| } | } | ||||
| } | } | ||||
| @@ -22,9 +22,7 @@ import org.springframework.stereotype.Component; | |||||
| import javax.annotation.PostConstruct; | import javax.annotation.PostConstruct; | ||||
| import java.io.BufferedReader; | import java.io.BufferedReader; | ||||
| import java.io.InputStreamReader; | import java.io.InputStreamReader; | ||||
| import java.util.HashMap; | |||||
| import java.util.LinkedHashMap; | |||||
| import java.util.Map; | |||||
| import java.util.*; | |||||
| /** | /** | ||||
| * k8s客户端 | * k8s客户端 | ||||
| @@ -282,13 +280,13 @@ public class K8sClientUtil { | |||||
| .endSpec() | .endSpec() | ||||
| .build(); | .build(); | ||||
| try { | try { | ||||
| pod = api.createNamespacedPod(namespace, pod, null, null, null); | pod = api.createNamespacedPod(namespace, pod, null, null, null); | ||||
| } catch (ApiException e) { | } catch (ApiException e) { | ||||
| log.error("创建pod异常:" + e.getResponseBody(), e); | log.error("创建pod异常:" + e.getResponseBody(), e); | ||||
| } catch (Exception e) { | } catch (Exception e) { | ||||
| log.error("创建pod系统异常:", e); | log.error("创建pod系统异常:", e); | ||||
| } | } | ||||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | V1Service service = createService(namespace, podName + "-svc", port, selector); | ||||
| @@ -324,7 +322,6 @@ public class K8sClientUtil { | |||||
| for (V1Pod pod1 : v1PodList.getItems()) { | for (V1Pod pod1 : v1PodList.getItems()) { | ||||
| if (StringUtils.equals(pod1.getMetadata().getName(), podName)) { | if (StringUtils.equals(pod1.getMetadata().getName(), podName)) { | ||||
| // PVC 已存在 | // PVC 已存在 | ||||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | V1Service service = createService(namespace, podName + "-svc", port, selector); | ||||
| if (service != null) { | if (service != null) { | ||||
| return service.getSpec().getPorts().get(0).getNodePort(); | return service.getSpec().getPorts().get(0).getNodePort(); | ||||
| @@ -378,6 +375,73 @@ public class K8sClientUtil { | |||||
| return service.getSpec().getPorts().get(0).getNodePort(); | 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 | * 根据获取namespace,deploymentName的Pod Name | ||||
| * | * | ||||
| @@ -495,4 +559,51 @@ public class K8sClientUtil { | |||||
| } | } | ||||
| return pod; | 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | } | ||||