1、添加选择镜像启动环境功能。

2、pod选择GPU节点部署。
3、增加保存镜像接口。
This commit is contained in:
西大锐 2024-08-27 15:55:13 +08:00
parent 8d8ad03f30
commit c594dca0ec
9 changed files with 196 additions and 10 deletions

View File

@ -140,7 +140,12 @@
<dependency>
<groupId>com.github.docker-java</groupId>
<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>
<groupId>com.baomidou</groupId>
@ -216,6 +221,12 @@
<version>3.0.8</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@ -78,16 +78,15 @@ public class ImageController extends BaseController {
* @param image 实体
* @return 新增结果
*/
@PostMapping
@ApiOperation("新增镜像,不包含镜像版本")
public GenericsAjaxResult<Image> add(@RequestBody Image image) {
return genericsSuccess(this.imageService.insert(image));
}
/**
* 新增镜像和版本
*
* @param imageVo 实体
* 新增镜像和版本 @PostMapping
* @return 新增结果
*/
@PostMapping("/addImageAndVersion")
@ -149,5 +148,11 @@ public class ImageController extends BaseController {
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));
}
}

View File

@ -93,6 +93,5 @@ public interface ImageService {
Map<String, String> createImageFromNet(String imageName, String imageTag, String NetPath) throws Exception;
Map<String, String> uploadImageFiles(MultipartFile file) throws Exception;
String saveImage(ImageVo imageVo);
}

View File

@ -9,12 +9,14 @@ import com.ruoyi.platform.mapper.ImageVersionDao;
import com.ruoyi.platform.service.ImageService;
import com.ruoyi.platform.service.ImageVersionService;
import com.ruoyi.platform.service.MinioService;
import com.ruoyi.platform.utils.DockerClientUtil;
import com.ruoyi.platform.utils.FileUtil;
import com.ruoyi.platform.utils.K8sClientUtil;
import com.ruoyi.platform.vo.ImageVo;
import com.ruoyi.system.api.model.LoginUser;
import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim;
import io.kubernetes.client.openapi.models.V1Pod;
import lombok.Synchronized;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
@ -50,6 +52,9 @@ public class ImageServiceImpl implements ImageService {
private ImageVersionDao imageVersionDao;
@Resource
private K8sClientUtil k8sClientUtil;
@Resource
private DockerClientUtil dockerClientUtil;
@Resource
private MinioService minioService;
@Value("${harbor.bucketName}")
@ -75,6 +80,8 @@ public class ImageServiceImpl implements ImageService {
private String proxyUrl;
@Value("${minio.pvcName}")
private String pvcName;
@Value("${jupyter.namespace}")
private String namespace;
/**
* 通过ID查询单条数据
*
@ -350,4 +357,26 @@ public class ImageServiceImpl implements ImageService {
String path = loginUser.getUsername()+"/"+file.getOriginalFilename();
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;
}
}

View File

@ -100,7 +100,7 @@ public class JupyterServiceImpl implements JupyterService {
//TODO 设置镜像可配置这里先用默认镜像启动pod
// 调用修改后的 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;
redisService.setCacheObject(podName,masterIp + ":" + podPort);
devEnvironment.setStatus("Pending");

View File

@ -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);
}
}
}

View File

@ -136,7 +136,7 @@ public class K8sClientUtil {
if (v1ServiceList!=null) {
for (V1Service svc : v1ServiceList.getItems()) {
if (StringUtils.equals(svc.getMetadata().getName(), serviceName)) {
// PVC 已存在
// SVC 已存在
return svc;
}
}
@ -380,6 +380,9 @@ public class K8sClientUtil {
Map<String, String> selector = new LinkedHashMap<>();
selector.put("k8s-jupyter", podName);
Map<String, String> nodeSelector = new LinkedHashMap<>();
nodeSelector.put("resource-type", "CPU-GPU");
CoreV1Api api = new CoreV1Api(apiClient);
V1PodList v1PodList = null;
try {
@ -423,6 +426,7 @@ public class K8sClientUtil {
.withVolumeMounts(volumeMounts)
.endContainer()
.withVolumes(volumes)
.withNodeSelector(nodeSelector)
.endSpec()
.build();
@ -506,6 +510,28 @@ public class K8sClientUtil {
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) {
CoreV1Api api = new CoreV1Api(apiClient);
try {

View File

@ -43,7 +43,7 @@ public class ImageVo implements Serializable {
/**
* 镜像tag名称
*/
@ApiModelProperty(name = "tag_name")
@ApiModelProperty(name = "tagName")
private String tagName;
/**
@ -64,6 +64,8 @@ public class ImageVo implements Serializable {
private String path;
@ApiModelProperty(value = "环境id")
private String devEnvironmentId;
// public Integer getId() {
// return id;
// }
@ -147,7 +149,17 @@ public class ImageVo implements Serializable {
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getDevEnvironmentId() {
return devEnvironmentId;
}
public void setDevEnvironmentId(String devEnvironmentId) {
this.devEnvironmentId = devEnvironmentId;
}
}

View File

@ -65,7 +65,7 @@
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- RuoYi Common Log -->
<dependency>
<groupId>com.ruoyi</groupId>
@ -77,7 +77,13 @@
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-swagger</artifactId>
</dependency>
<!-- RuoYi Common DataSource -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-datasource</artifactId>
</dependency>
</dependencies>
<build>