| @@ -140,7 +140,12 @@ | |||||
| <dependency> | <dependency> | ||||
| <groupId>com.github.docker-java</groupId> | <groupId>com.github.docker-java</groupId> | ||||
| <artifactId>docker-java</artifactId> | <artifactId>docker-java</artifactId> | ||||
| <version>3.1.1</version> | |||||
| <version>3.2.13</version> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>com.github.docker-java</groupId> | |||||
| <artifactId>docker-java-transport-httpclient5</artifactId> | |||||
| <version>3.2.13</version> | |||||
| </dependency> | </dependency> | ||||
| <dependency> | <dependency> | ||||
| <groupId>com.baomidou</groupId> | <groupId>com.baomidou</groupId> | ||||
| @@ -216,6 +221,12 @@ | |||||
| <version>3.0.8</version> | <version>3.0.8</version> | ||||
| <scope>compile</scope> | <scope>compile</scope> | ||||
| </dependency> | </dependency> | ||||
| <dependency> | |||||
| <groupId>commons-lang</groupId> | |||||
| <artifactId>commons-lang</artifactId> | |||||
| <version>2.6</version> | |||||
| <scope>compile</scope> | |||||
| </dependency> | |||||
| </dependencies> | </dependencies> | ||||
| @@ -78,16 +78,15 @@ public class ImageController extends BaseController { | |||||
| * @param image 实体 | * @param image 实体 | ||||
| * @return 新增结果 | * @return 新增结果 | ||||
| */ | */ | ||||
| @PostMapping | |||||
| @ApiOperation("新增镜像,不包含镜像版本") | @ApiOperation("新增镜像,不包含镜像版本") | ||||
| public GenericsAjaxResult<Image> add(@RequestBody Image image) { | public GenericsAjaxResult<Image> add(@RequestBody Image image) { | ||||
| return genericsSuccess(this.imageService.insert(image)); | return genericsSuccess(this.imageService.insert(image)); | ||||
| } | } | ||||
| /** | /** | ||||
| * 新增镜像和版本 | |||||
| * | * | ||||
| * @param imageVo 实体 | * @param imageVo 实体 | ||||
| * 新增镜像和版本 @PostMapping | |||||
| * @return 新增结果 | * @return 新增结果 | ||||
| */ | */ | ||||
| @PostMapping("/addImageAndVersion") | @PostMapping("/addImageAndVersion") | ||||
| @@ -149,5 +148,11 @@ public class ImageController extends BaseController { | |||||
| return genericsSuccess(this.imageService.uploadImageFiles(file)); | return genericsSuccess(this.imageService.uploadImageFiles(file)); | ||||
| } | } | ||||
| @PostMapping("/saveImage") | |||||
| @ApiOperation(value = "保存环境为镜像", notes = "docker commit方式保存,并推送到horbor") | |||||
| public GenericsAjaxResult<String> saveImage(ImageVo imageVo){ | |||||
| return genericsSuccess(this.imageService.saveImage(imageVo)); | |||||
| } | |||||
| } | } | ||||
| @@ -93,6 +93,5 @@ public interface ImageService { | |||||
| Map<String, String> createImageFromNet(String imageName, String imageTag, String NetPath) throws Exception; | Map<String, String> createImageFromNet(String imageName, String imageTag, String NetPath) throws Exception; | ||||
| Map<String, String> uploadImageFiles(MultipartFile file) throws Exception; | Map<String, String> uploadImageFiles(MultipartFile file) throws Exception; | ||||
| String saveImage(ImageVo imageVo); | |||||
| } | } | ||||
| @@ -9,12 +9,14 @@ import com.ruoyi.platform.mapper.ImageVersionDao; | |||||
| import com.ruoyi.platform.service.ImageService; | import com.ruoyi.platform.service.ImageService; | ||||
| import com.ruoyi.platform.service.ImageVersionService; | import com.ruoyi.platform.service.ImageVersionService; | ||||
| import com.ruoyi.platform.service.MinioService; | import com.ruoyi.platform.service.MinioService; | ||||
| import com.ruoyi.platform.utils.DockerClientUtil; | |||||
| import com.ruoyi.platform.utils.FileUtil; | import com.ruoyi.platform.utils.FileUtil; | ||||
| import com.ruoyi.platform.utils.K8sClientUtil; | import com.ruoyi.platform.utils.K8sClientUtil; | ||||
| import com.ruoyi.platform.vo.ImageVo; | import com.ruoyi.platform.vo.ImageVo; | ||||
| import com.ruoyi.system.api.model.LoginUser; | import com.ruoyi.system.api.model.LoginUser; | ||||
| import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; | import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; | ||||
| import io.kubernetes.client.openapi.models.V1Pod; | import io.kubernetes.client.openapi.models.V1Pod; | ||||
| import lombok.Synchronized; | |||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||
| import org.springframework.beans.factory.annotation.Value; | import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.data.domain.Page; | import org.springframework.data.domain.Page; | ||||
| @@ -50,6 +52,9 @@ public class ImageServiceImpl implements ImageService { | |||||
| private ImageVersionDao imageVersionDao; | private ImageVersionDao imageVersionDao; | ||||
| @Resource | @Resource | ||||
| private K8sClientUtil k8sClientUtil; | private K8sClientUtil k8sClientUtil; | ||||
| @Resource | |||||
| private DockerClientUtil dockerClientUtil; | |||||
| @Resource | @Resource | ||||
| private MinioService minioService; | private MinioService minioService; | ||||
| @Value("${harbor.bucketName}") | @Value("${harbor.bucketName}") | ||||
| @@ -75,6 +80,8 @@ public class ImageServiceImpl implements ImageService { | |||||
| private String proxyUrl; | private String proxyUrl; | ||||
| @Value("${minio.pvcName}") | @Value("${minio.pvcName}") | ||||
| private String pvcName; | private String pvcName; | ||||
| @Value("${jupyter.namespace}") | |||||
| private String namespace; | |||||
| /** | /** | ||||
| * 通过ID查询单条数据 | * 通过ID查询单条数据 | ||||
| * | * | ||||
| @@ -350,4 +357,26 @@ public class ImageServiceImpl implements ImageService { | |||||
| String path = loginUser.getUsername()+"/"+file.getOriginalFilename(); | String path = loginUser.getUsername()+"/"+file.getOriginalFilename(); | ||||
| return minioService.uploadFile(bucketName, path, file); | return minioService.uploadFile(bucketName, path, file); | ||||
| } | } | ||||
| @Override | |||||
| @Synchronized | |||||
| public String saveImage(ImageVo imageVo) { | |||||
| if(imageDao.getByName(imageVo.getName()) != null){ | |||||
| throw new IllegalStateException("镜像名称已存在"); | |||||
| } | |||||
| LoginUser loginUser = SecurityUtils.getLoginUser(); | |||||
| String username = loginUser.getUsername().toLowerCase(); | |||||
| String podName = username +"-editor-pod" + "-" + imageVo.getDevEnvironmentId(); | |||||
| try { | |||||
| String containerId = k8sClientUtil.getPodContainerId(podName, namespace); | |||||
| String hostIp = k8sClientUtil.getHostIp(podName, namespace); | |||||
| dockerClientUtil.commitImage(imageVo,containerId,hostIp,username); | |||||
| dockerClientUtil.pushImageToHorbor(imageVo,hostIp); | |||||
| } catch (Exception e) { | |||||
| throw new RuntimeException(e); | |||||
| } | |||||
| return null; | |||||
| } | |||||
| } | } | ||||
| @@ -100,7 +100,7 @@ public class JupyterServiceImpl implements JupyterService { | |||||
| //TODO 设置镜像可配置,这里先用默认镜像启动pod | //TODO 设置镜像可配置,这里先用默认镜像启动pod | ||||
| // 调用修改后的 createPod 方法,传入额外的参数 | // 调用修改后的 createPod 方法,传入额外的参数 | ||||
| Integer podPort = k8sClientUtil.createConfiguredPod(podName, namespace, port, mountPath, pvc, image, minioPvcName, datasetPath, modelPath); | |||||
| Integer podPort = k8sClientUtil.createConfiguredPod(podName, namespace, port, mountPath, pvc, devEnvironment.getImage(), minioPvcName, datasetPath, modelPath); | |||||
| String url = masterIp + ":" + podPort; | String url = masterIp + ":" + podPort; | ||||
| redisService.setCacheObject(podName,masterIp + ":" + podPort); | redisService.setCacheObject(podName,masterIp + ":" + podPort); | ||||
| devEnvironment.setStatus("Pending"); | devEnvironment.setStatus("Pending"); | ||||
| @@ -0,0 +1,98 @@ | |||||
| package com.ruoyi.platform.utils; | |||||
| import com.github.dockerjava.api.DockerClient; | |||||
| import com.github.dockerjava.api.command.CommitCmd; | |||||
| import com.github.dockerjava.api.model.AuthConfig; | |||||
| import com.github.dockerjava.core.DefaultDockerClientConfig; | |||||
| import com.github.dockerjava.core.DockerClientConfig; | |||||
| import com.github.dockerjava.core.DockerClientImpl; | |||||
| import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; | |||||
| import com.github.dockerjava.transport.DockerHttpClient; | |||||
| import com.ruoyi.platform.vo.ImageVo; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.springframework.beans.factory.annotation.Value; | |||||
| import org.springframework.stereotype.Component; | |||||
| import java.time.Duration; | |||||
| @Slf4j | |||||
| @Component | |||||
| public class DockerClientUtil { | |||||
| @Value("${harbor.bucketName}") | |||||
| private String bucketName; | |||||
| @Value("${harbor.repository}") | |||||
| private String repository; | |||||
| @Value("${harbor.harborUrl}") | |||||
| private String harborUrl; | |||||
| @Value("${harbor.harborUser}") | |||||
| private String harborUser; | |||||
| @Value("${harbor.harborpassword}") | |||||
| private String harborpassword; | |||||
| public DockerClient getDockerClient(String dockerServerUrl) { | |||||
| //创建DefaultDockerClientConfig(指定docker服务器的配置) | |||||
| DockerClientConfig config = DefaultDockerClientConfig | |||||
| .createDefaultConfigBuilder() | |||||
| .withDockerHost("tcp://"+ dockerServerUrl +":2375") | |||||
| .withDockerTlsVerify(false) | |||||
| .withApiVersion("1.40") | |||||
| // .withDockerCertPath(dcokerCertPath) | |||||
| // .withRegistryUsername(registryUser) | |||||
| // .withRegistryPassword(registryPass) | |||||
| // .withRegistryEmail(registryMail) | |||||
| // .withRegistryUrl(registryUrl) | |||||
| .build(); | |||||
| //创建DockerHttpClient | |||||
| DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() | |||||
| .dockerHost(config.getDockerHost()) | |||||
| .sslConfig(config.getSSLConfig()) | |||||
| .maxConnections(1000) | |||||
| .connectionTimeout(Duration.ofSeconds(300)) | |||||
| .responseTimeout(Duration.ofSeconds(450)) | |||||
| .build(); | |||||
| //创建DockerClient | |||||
| return DockerClientImpl.getInstance(config, httpClient); | |||||
| } | |||||
| public String commitImage(ImageVo imageVo, String containerId, String hostIp, String userName) { | |||||
| DockerClient dockerClient = getDockerClient(hostIp); | |||||
| // dockerClient.startContainerCmd(containerId).exec(); | |||||
| // 提交容器为镜像,这里的"new_image"和"new_tag"是新镜像的名字和标签 | |||||
| CommitCmd commitCmd = dockerClient.commitCmd(containerId) | |||||
| .withRepository(imageVo.getName()) | |||||
| .withTag(imageVo.getTagName()) | |||||
| .withAuthor(userName) | |||||
| .withMessage(imageVo.getDescription()); | |||||
| String exec = commitCmd.exec(); | |||||
| return exec; | |||||
| } | |||||
| public void pushImageToHorbor(ImageVo imageVo, String hostIp) { | |||||
| DockerClient dockerClient = getDockerClient(hostIp); | |||||
| //Harbor登录信息 | |||||
| AuthConfig autoConfig = new AuthConfig().withRegistryAddress(harborUrl).withUsername(harborUser).withPassword(harborpassword); | |||||
| String localImageName = imageVo.getName() + ":" + imageVo.getTagName(); | |||||
| String imageName = harborUrl + "/" + bucketName + "/" + imageVo.getName(); | |||||
| //给镜像打上tag | |||||
| dockerClient.tagImageCmd(localImageName, imageName, imageVo.getTagName()).exec(); | |||||
| //推送镜像至镜像仓库 | |||||
| try { | |||||
| dockerClient.pushImageCmd(imageName).withAuthConfig(autoConfig).start().awaitCompletion(); | |||||
| //push成功后,删除本地加载的镜像 | |||||
| dockerClient.removeImageCmd(localImageName).exec(); | |||||
| } catch (InterruptedException e) { | |||||
| throw new RuntimeException("推送镜像失败:"+e); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -136,7 +136,7 @@ public class K8sClientUtil { | |||||
| if (v1ServiceList!=null) { | if (v1ServiceList!=null) { | ||||
| for (V1Service svc : v1ServiceList.getItems()) { | for (V1Service svc : v1ServiceList.getItems()) { | ||||
| if (StringUtils.equals(svc.getMetadata().getName(), serviceName)) { | if (StringUtils.equals(svc.getMetadata().getName(), serviceName)) { | ||||
| // PVC 已存在 | |||||
| // SVC 已存在 | |||||
| return svc; | return svc; | ||||
| } | } | ||||
| } | } | ||||
| @@ -380,6 +380,9 @@ public class K8sClientUtil { | |||||
| Map<String, String> selector = new LinkedHashMap<>(); | Map<String, String> selector = new LinkedHashMap<>(); | ||||
| selector.put("k8s-jupyter", podName); | selector.put("k8s-jupyter", podName); | ||||
| Map<String, String> nodeSelector = new LinkedHashMap<>(); | |||||
| nodeSelector.put("resource-type", "CPU-GPU"); | |||||
| CoreV1Api api = new CoreV1Api(apiClient); | CoreV1Api api = new CoreV1Api(apiClient); | ||||
| V1PodList v1PodList = null; | V1PodList v1PodList = null; | ||||
| try { | try { | ||||
| @@ -423,6 +426,7 @@ public class K8sClientUtil { | |||||
| .withVolumeMounts(volumeMounts) | .withVolumeMounts(volumeMounts) | ||||
| .endContainer() | .endContainer() | ||||
| .withVolumes(volumes) | .withVolumes(volumes) | ||||
| .withNodeSelector(nodeSelector) | |||||
| .endSpec() | .endSpec() | ||||
| .build(); | .build(); | ||||
| @@ -506,6 +510,28 @@ public class K8sClientUtil { | |||||
| return pod.getStatus().getPhase(); | return pod.getStatus().getPhase(); | ||||
| } | } | ||||
| /** | |||||
| * 根据Pod的名称和Namespace查询Pod的容器信息 | |||||
| * @param podName Pod的名称 | |||||
| * @param namespace Pod所在的Namespace | |||||
| */ | |||||
| public String getPodContainerId(String podName, String namespace) throws Exception { | |||||
| CoreV1Api api = new CoreV1Api(apiClient); | |||||
| V1Pod pod = api.readNamespacedPod(podName, namespace, null, null, null); | |||||
| if(pod.getStatus().getContainerStatuses().size() !=1){ | |||||
| throw new RuntimeException("容器错误"); | |||||
| } | |||||
| String containerId = pod.getStatus().getContainerStatuses().get(0).getContainerID().split("//")[1]; | |||||
| return containerId; | |||||
| } | |||||
| public String getHostIp(String podName, String namespace) throws Exception { | |||||
| CoreV1Api api = new CoreV1Api(apiClient); | |||||
| V1Pod pod = api.readNamespacedPod(podName, namespace, null, null, null); | |||||
| return pod.getStatus().getHostIP(); | |||||
| } | |||||
| public String getPodLogs(String podName,String namespace,String container,int line) { | public String getPodLogs(String podName,String namespace,String container,int line) { | ||||
| CoreV1Api api = new CoreV1Api(apiClient); | CoreV1Api api = new CoreV1Api(apiClient); | ||||
| try { | try { | ||||
| @@ -43,7 +43,7 @@ public class ImageVo implements Serializable { | |||||
| /** | /** | ||||
| * 镜像tag名称 | * 镜像tag名称 | ||||
| */ | */ | ||||
| @ApiModelProperty(name = "tag_name") | |||||
| @ApiModelProperty(name = "tagName") | |||||
| private String tagName; | private String tagName; | ||||
| /** | /** | ||||
| @@ -64,6 +64,8 @@ public class ImageVo implements Serializable { | |||||
| private String path; | private String path; | ||||
| @ApiModelProperty(value = "环境id") | |||||
| private String devEnvironmentId; | |||||
| // public Integer getId() { | // public Integer getId() { | ||||
| // return id; | // return id; | ||||
| // } | // } | ||||
| @@ -147,7 +149,17 @@ public class ImageVo implements Serializable { | |||||
| public String getPath() { | public String getPath() { | ||||
| return path; | return path; | ||||
| } | } | ||||
| public void setPath(String path) { | public void setPath(String path) { | ||||
| this.path = path; | this.path = path; | ||||
| } | } | ||||
| public String getDevEnvironmentId() { | |||||
| return devEnvironmentId; | |||||
| } | |||||
| public void setDevEnvironmentId(String devEnvironmentId) { | |||||
| this.devEnvironmentId = devEnvironmentId; | |||||
| } | |||||
| } | } | ||||
| @@ -65,7 +65,7 @@ | |||||
| <groupId>com.mysql</groupId> | <groupId>com.mysql</groupId> | ||||
| <artifactId>mysql-connector-j</artifactId> | <artifactId>mysql-connector-j</artifactId> | ||||
| </dependency> | </dependency> | ||||
| <!-- RuoYi Common Log --> | <!-- RuoYi Common Log --> | ||||
| <dependency> | <dependency> | ||||
| <groupId>com.ruoyi</groupId> | <groupId>com.ruoyi</groupId> | ||||
| @@ -77,7 +77,13 @@ | |||||
| <groupId>com.ruoyi</groupId> | <groupId>com.ruoyi</groupId> | ||||
| <artifactId>ruoyi-common-swagger</artifactId> | <artifactId>ruoyi-common-swagger</artifactId> | ||||
| </dependency> | </dependency> | ||||
| <!-- RuoYi Common DataSource --> | |||||
| <dependency> | |||||
| <groupId>com.ruoyi</groupId> | |||||
| <artifactId>ruoyi-common-datasource</artifactId> | |||||
| </dependency> | |||||
| </dependencies> | </dependencies> | ||||
| <build> | <build> | ||||