diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index aeccedfa..18f66e69 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -63,6 +63,9 @@
+
+
+
diff --git a/dependencies/.flattened-pom.xml b/dependencies/.flattened-pom.xml
index 3c5f83c2..b7224864 100644
--- a/dependencies/.flattened-pom.xml
+++ b/dependencies/.flattened-pom.xml
@@ -13,6 +13,7 @@
8.0.0.RELEASE
1.6.0
5.1.0
+ 4.12.0
1.6.2
1.2.83
3.47.1.0
@@ -52,7 +53,7 @@
3.12.1
1.2.13
1.4.13
- 3.5.0
+ 3.2.13
5.8.32
2.0.0-jdk17
0.1.55
@@ -573,11 +574,32 @@
com.github.docker-java
docker-java
${docker-java.version}
+
+
+ com.google.guava
+ guava
+
+
+ commons-io
+ commons-io
+
+
com.github.docker-java
docker-java-transport-httpclient5
${docker-java.version}
+
+
+ org.slf4j
+ slf4j-log4j12
+
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp.version}
org.jvnet.winp
diff --git a/dependencies/pom.xml b/dependencies/pom.xml
index 12bc9668..75c0a7f9 100644
--- a/dependencies/pom.xml
+++ b/dependencies/pom.xml
@@ -68,6 +68,7 @@
3.0.6
4.1.113.Final
6.6.5
+ 4.12.0
2.17.0
1.27.1
@@ -656,14 +657,33 @@
com.github.docker-java
docker-java
${docker-java.version}
+
+
+ guava
+ com.google.guava
+
+
+ commons-io
+ commons-io
+
+
com.github.docker-java
docker-java-transport-httpclient5
${docker-java.version}
+
+
+ org.slf4j
+ slf4j-log4j12
+
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp.version}
-
-
org.jvnet.winp
diff --git a/framework/spring-boot-starter-test/src/main/java/cd/casic/framework/test/core/ut/BaseDbUnitTest.java b/framework/spring-boot-starter-test/src/main/java/cd/casic/framework/test/core/ut/BaseDbUnitTest.java
index 5d7a63eb..44235be4 100644
--- a/framework/spring-boot-starter-test/src/main/java/cd/casic/framework/test/core/ut/BaseDbUnitTest.java
+++ b/framework/spring-boot-starter-test/src/main/java/cd/casic/framework/test/core/ut/BaseDbUnitTest.java
@@ -37,7 +37,6 @@ public class BaseDbUnitTest {
OpsMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
MybatisPlusJoinAutoConfiguration.class, // MyBatis 的Join配置类
-
// 其它配置类
SpringUtil.class
})
diff --git a/modules/module-ci-execute/Readme.md b/modules/module-ci-execute/Readme.md
new file mode 100644
index 00000000..da5f0fee
--- /dev/null
+++ b/modules/module-ci-execute/Readme.md
@@ -0,0 +1,7 @@
+这个目前制作了docker 的服务,
+问题一:目的是之前使用ssh进行cmd命令,但是对话框中要使用docker exec/attch等进入容器执行对象的命令,这种方式不太理想
+问题二:如afl等之类的场景,你只有等容器启动了后才能够知道你的容器id,name虽然可以进行提前设置,但是有冲突的风险,name冲突可以使用
+Ops+随机数+镜像名称解决,关闭时候需要输入命令docker rm -f xxx进行关闭,或者脚本,目前考虑使用docker 的api进行操作
+问题三:容器的启动,停止等操作,缺乏统一的操作,同时缺少容器的监控方面内容
+问题四:容器的日志方面,同问题一一样,需要获取到日志,依赖ssh,存在问题
+问题五:最好其实和k8s一样,使用go这种云原生的方式,但是这种方式增加了运维的难度,暂时都放到一起,为了方便部署
\ No newline at end of file
diff --git a/modules/module-ci-execute/pom.xml b/modules/module-ci-execute/pom.xml
index 18b0ff8d..8739cff9 100644
--- a/modules/module-ci-execute/pom.xml
+++ b/modules/module-ci-execute/pom.xml
@@ -19,22 +19,37 @@
cd.casic.boot
commons
-
+
+ cd.casic.boot
+ spring-boot-starter-security
+
cd.casic.boot
spring-boot-starter-test
-
com.github.docker-java
docker-java
-
com.github.docker-java
docker-java-transport-httpclient5
-
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ 5.4
+
+
+ org.apache.httpcomponents.core5
+ httpcore5
+ 5.3.4
+
+
+ commons-io
+ commons-io
+ 2.17.0
+
\ No newline at end of file
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/DockerClientFactory.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/DockerClientFactory.java
new file mode 100644
index 00000000..cc3696e5
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/DockerClientFactory.java
@@ -0,0 +1,76 @@
+package cd.casic.module.execute.docker;
+
+import cd.casic.module.execute.docker.dao.DockerEndpointDao;
+import cd.casic.module.execute.docker.dataobject.convert.DockerEndpointConvert;
+import cd.casic.module.execute.docker.dataobject.model.DockerEndpoint;
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.core.DockerClientBuilder;
+import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
+import jakarta.annotation.Resource;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static java.lang.String.format;
+
+/**
+ * @description: docker客户端工厂
+ * @author: mianbin
+ * @date: 2025/5/26 10:39
+ * @version: 1.0
+ */
+@Slf4j
+@Component
+public class DockerClientFactory implements CommandLineRunner {
+
+ @Resource
+ private DockerEndpointDao dockerEndpointDao;
+ @Getter
+ private final Map clientGroup = new ConcurrentHashMap<>();
+
+ @Override
+ public void run(String... args) throws Exception {
+ /*如果在ops里面之前添加过,数据库之前有,那么启动就加载,如果是手动添加的,后面要注意控制*/
+ dockerEndpointDao.selectList().stream()
+ .map(DockerEndpointConvert.INSTANCE::convert)
+ .forEach(endpoint -> {
+ ApacheDockerHttpClient httpClient = createHttpClient(endpoint);
+ if (httpClient != null) {
+ DockerClient client = DockerClientBuilder.getInstance().withDockerHttpClient(httpClient).build();
+ clientGroup.put(endpoint.getId(), client);
+ }
+ });
+ }
+
+ private ApacheDockerHttpClient createHttpClient(DockerEndpoint endpoint) {
+ try {
+ URI dockerHost;
+ if (endpoint.getType() == DockerEndpoint.DockerEndpointTypeEnum.LOCAL) {
+ // 使用本地挂载
+ dockerHost = new URI("unix:///var/run/docker.sock");
+ } else if (endpoint.getType() == DockerEndpoint.DockerEndpointTypeEnum.REMOTE) {
+ // 远程挂载
+ dockerHost = new URI(format("tcp://%s:%s", endpoint.getHost(), endpoint.getPort()));
+ } else {
+ log.error("Unsupported Docker endpoint type: {}", endpoint.getType());
+ return null;
+ }
+ return new ApacheDockerHttpClient.Builder()
+ .dockerHost(dockerHost)
+ .build();
+ } catch (URISyntaxException e) {
+ log.error("Failed to create URI for Docker endpoint {}: {}", endpoint.getId(), e.getMessage(), e);
+ return null;
+ }
+ }
+
+ public DockerClient getdockerClient(String id) {
+ return clientGroup.get(id);
+ }
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerClientController.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerClientController.java
new file mode 100644
index 00000000..10fa696a
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerClientController.java
@@ -0,0 +1,49 @@
+package cd.casic.module.execute.docker.api;
+
+import cd.casic.framework.commons.pojo.CommonResult;
+import cd.casic.module.execute.docker.DockerClientFactory;
+import cd.casic.module.execute.docker.dao.DockerEndpointDao;
+import cd.casic.module.execute.docker.dataobject.convert.DockerEndpointConvert;
+import cd.casic.module.execute.docker.dataobject.model.DockerEndpoint;
+import com.github.dockerjava.api.DockerClient;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * @description: 容器的管理功能
+ * @author: mianbin
+ * @date: 2025/5/26 11:14
+ * @version: 1.0
+ */
+@Tag(name = "docker管理 - 获取docker列表")
+@RestController
+@RequestMapping("/api/dockerClient")
+@RequiredArgsConstructor
+public class DockerClientController {
+
+ private final DockerEndpointDao dockerEndpointDao;
+ private final DockerClientFactory dockerClientFactory;
+
+ @GetMapping
+ public CommonResult> list() {
+ List collect = dockerEndpointDao
+ .selectList()
+ .stream()
+ .map(DockerEndpointConvert.INSTANCE::convert)
+ .toList();
+ return CommonResult.success(collect);
+ }
+
+ @GetMapping
+ public CommonResult testDockerClient(String dockerClientId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(dockerClientId);
+ dockerClient.pingCmd().exec();
+ return CommonResult.success(Boolean.TRUE);
+ }
+
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerContainerController.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerContainerController.java
new file mode 100644
index 00000000..9ebedc3f
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerContainerController.java
@@ -0,0 +1,24 @@
+package cd.casic.module.execute.docker.api;
+
+import cd.casic.module.execute.docker.DockerClientFactory;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 11:13
+ * @version: 1.0
+ */
+@Tag(name = "docker管理 - 容器管理")
+@RestController
+@RequestMapping("/api/container")
+@RequiredArgsConstructor
+public class DockerContainerController {
+
+ private final DockerClientFactory dockerClientFactory;
+
+
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerImageController.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerImageController.java
new file mode 100644
index 00000000..290ad6b7
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerImageController.java
@@ -0,0 +1,20 @@
+package cd.casic.module.execute.docker.api;
+
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 10:25
+ * @version: 1.0
+ */
+@Tag(name = "docker管理 - 镜像管理")
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/images")
+public class DockerImageController {
+
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerMonitorController.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerMonitorController.java
new file mode 100644
index 00000000..a51f5223
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerMonitorController.java
@@ -0,0 +1,10 @@
+package cd.casic.module.execute.docker.api;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 11:14
+ * @version: 1.0
+ */
+public class DockerMonitorController {
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/callback/LoggingCallback.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/callback/LoggingCallback.java
new file mode 100644
index 00000000..7d437519
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/callback/LoggingCallback.java
@@ -0,0 +1,57 @@
+package cd.casic.module.execute.docker.callback;
+
+import com.github.dockerjava.api.async.ResultCallback;
+import com.github.dockerjava.api.model.Frame;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @description: 之前版本用的ExecStartResultCallback实现,现在废弃了,使用ResultCallback.Adapter,未测试
+ * @author: mianbin
+ * @date: 2025/5/26 18:09
+ * @version: 1.0
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class LoggingCallback extends ResultCallback.Adapter {
+
+ private final String containerId;
+ private final String execId;
+ private StringBuffer buffer = new StringBuffer(1024);
+
+ @Override
+ public void onNext(Frame frame) {
+ String streamType = frame.getStreamType().name();
+ String message = new String(frame.getPayload(), StandardCharsets.UTF_8);
+ if (streamType.equals("STDOUT")) {
+ log.info("[容器: {}, ExecID: {}] 标准输出: {}", containerId, execId, message.trim());
+ } else if (streamType.equals("STDERR")) {
+ log.error("[容器: {}, ExecID: {}] 错误输出: {}", containerId, execId, message.trim());
+ }
+ buffer.append(message);
+ super.onNext(frame);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ log.error("[容器: {}, ExecID: {}] 执行命令时出错: {}", containerId, execId, throwable.getMessage());
+ super.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ log.info("[容器: {}, ExecID: {}] 命令执行完毕", containerId, execId);
+ super.onComplete();
+ }
+
+ @Override
+ public void close() throws IOException {
+
+ log.debug("[容器: {}, ExecID: {}] 回调已关闭", containerId, execId);
+ super.close();
+ }
+
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dao/DockerEndpointDao.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dao/DockerEndpointDao.java
new file mode 100644
index 00000000..02c7bb71
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dao/DockerEndpointDao.java
@@ -0,0 +1,15 @@
+package cd.casic.module.execute.docker.dao;
+
+import cd.casic.framework.mybatis.core.mapper.BaseMapperX;
+import cd.casic.module.execute.docker.dataobject.dto.DockerEndpointDo;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 10:33
+ * @version: 1.0
+ */
+@Mapper
+public interface DockerEndpointDao extends BaseMapperX {
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dao/OperateRecordDao.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dao/OperateRecordDao.java
new file mode 100644
index 00000000..b6b0a435
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dao/OperateRecordDao.java
@@ -0,0 +1,15 @@
+package cd.casic.module.execute.docker.dao;
+
+import cd.casic.framework.mybatis.core.mapper.BaseMapperX;
+import cd.casic.module.execute.docker.dataobject.dto.OperateRecordDo;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 16:58
+ * @version: 1.0
+ */
+@Mapper
+public interface OperateRecordDao extends BaseMapperX {
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/convert/DockerEndpointConvert.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/convert/DockerEndpointConvert.java
new file mode 100644
index 00000000..fac3ef5a
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/convert/DockerEndpointConvert.java
@@ -0,0 +1,28 @@
+package cd.casic.module.execute.docker.dataobject.convert;
+
+import cd.casic.module.execute.docker.dao.DockerEndpointDao;
+import cd.casic.module.execute.docker.dataobject.dto.DockerEndpointDo;
+import cd.casic.module.execute.docker.dataobject.model.DockerEndpoint;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 10:36
+ * @version: 1.0
+ */
+@Mapper
+public interface DockerEndpointConvert {
+ DockerEndpointConvert INSTANCE = Mappers.getMapper(DockerEndpointConvert.class);
+
+ DockerEndpointDo convert(DockerEndpointDao endpointDao);
+
+ DockerEndpoint convert(DockerEndpointDo endpointDo);
+
+ List convertList(List endpointDos);
+
+ List convertList02(List endpointDos);
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/convert/OperateRecordConvert.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/convert/OperateRecordConvert.java
new file mode 100644
index 00000000..b9be47b2
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/convert/OperateRecordConvert.java
@@ -0,0 +1,15 @@
+package cd.casic.module.execute.docker.dataobject.convert;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 16:57
+ * @version: 1.0
+ */
+@Mapper
+public interface OperateRecordConvert {
+ OperateRecordConvert INSTANCE = Mappers.getMapper(OperateRecordConvert.class);
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/dto/DockerEndpointDo.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/dto/DockerEndpointDo.java
new file mode 100644
index 00000000..1ae91d8d
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/dto/DockerEndpointDo.java
@@ -0,0 +1,40 @@
+package cd.casic.module.execute.docker.dataobject.dto;
+
+import cd.casic.framework.mybatis.core.dataobject.BaseDO;
+import cd.casic.module.execute.docker.dataobject.model.DockerEndpoint;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+/**
+ * @description: docker 的端点配置
+ * @author: mianbin
+ * @date: 2025/5/26 10:28
+ * @version: 1.0
+ */
+@TableName("pipeline_docker_endpoint")
+@Data
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = true)
+public class DockerEndpointDo extends BaseDO {
+
+ @TableId
+ private String id;
+
+ private String name;
+
+ private DockerEndpoint.DockerEndpointTypeEnum type;
+
+ private DockerEndpoint.DockerEndpointStateEnum state;
+
+ private String host;
+
+ private Integer port;
+
+ private LocalDateTime latestTestTime;
+
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/dto/OperateRecordDo.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/dto/OperateRecordDo.java
new file mode 100644
index 00000000..b2dd6837
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/dto/OperateRecordDo.java
@@ -0,0 +1,36 @@
+package cd.casic.module.execute.docker.dataobject.dto;
+
+import cd.casic.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import static cd.casic.module.execute.docker.dataobject.model.OperateRecord.OperatorResource;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 16:48
+ * @version: 1.0
+ */
+@TableName("pipeline_docker_record")
+@Data
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = true)
+public class OperateRecordDo extends BaseDO {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Long clientId;
+
+ private Long userId;
+
+ private String name;
+
+ private OperatorResource resource;
+
+ private String content;
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/DockerEndpoint.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/DockerEndpoint.java
new file mode 100644
index 00000000..b5b21822
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/DockerEndpoint.java
@@ -0,0 +1,59 @@
+package cd.casic.module.execute.docker.dataobject.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+
+import java.util.Optional;
+
+/**
+ * @description: docker 的断电配置类
+ * @author: mianbin
+ * @date: 2025/5/26 10:11
+ * @version: 1.0
+ */
+@Data
+public class DockerEndpoint {
+ private String id;
+
+ private String name;
+
+ private DockerEndpointTypeEnum type;
+
+ private DockerEndpointStateEnum state = DockerEndpointStateEnum.NORMAL;
+
+ private String host;
+
+ private Integer port;
+
+ private String latestTestTime;
+
+ private String createdAt;
+
+ private String updatedAt;
+
+ @Getter
+ @AllArgsConstructor
+ public enum DockerEndpointTypeEnum {
+ LOCAL("本地"),
+ REMOTE("远程");
+ private final String desc;
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public enum DockerEndpointStateEnum {
+ NORMAL("正常"),
+ NOT_CONNECT("无法连接"),
+ AUTH_FAIL("授权失败");
+ private final String desc;
+ }
+
+ public String getTypeName() {
+ return Optional.ofNullable(this.type).map(DockerEndpointTypeEnum::getDesc).orElse("未知");
+ }
+
+ public String getStateName() {
+ return Optional.ofNullable(this.state).map(DockerEndpointStateEnum::getDesc).orElse("未知");
+ }
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/OperateRecord.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/OperateRecord.java
new file mode 100644
index 00000000..a722d33c
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/OperateRecord.java
@@ -0,0 +1,37 @@
+package cd.casic.module.execute.docker.dataobject.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.Getter;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 16:45
+ * @version: 1.0
+ */
+@Data
+@Builder
+public class OperateRecord {
+
+ private String clientId;
+
+ private Long userId;
+
+ private String name;
+
+ private OperatorResource resource;
+
+ private Object content;
+
+ @Getter
+ @AllArgsConstructor
+ public enum OperatorResource {
+ IMAGE_v1("操作镜像"),
+ CONTAINER_v1("操作容器"),
+ VOLUME_v1("操作储存卷"),
+ NETWORK_v1("操作网络");
+ private final String desc;
+ }
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/RunNewContainer.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/RunNewContainer.java
new file mode 100644
index 00000000..80c331b8
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/RunNewContainer.java
@@ -0,0 +1,62 @@
+package cd.casic.module.execute.docker.dataobject.model;
+
+import cn.hutool.core.collection.CollUtil;
+import lombok.Data;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @description: 新运行的请求类
+ * @author: mianbin
+ * @date: 2025/5/26 16:30
+ * @version: 1.0
+ */
+@Data
+public class RunNewContainer {
+ /**
+ * 使用的镜像ID
+ */
+ private String imageId;
+
+ /**
+ * 容器名称
+ */
+ private String containerName;
+
+ /**
+ * 环境变量
+ */
+ private Map envGroup;
+
+ /**
+ * 端口绑定
+ */
+ private Map portBound;
+
+ /**
+ * DNS
+ */
+ private String dns;
+
+ /**
+ * 容器别名
+ */
+ private String alias;
+
+ /**
+ * 主机名
+ */
+ private String hostname;
+
+
+ public List findEnvList() {
+ // 若 envGroup 为空,返回空列表;否则将 envGroup 转换为包含键值对字符串的列表
+ return CollUtil.isEmpty(this.envGroup) ? Collections.emptyList() :
+ this.envGroup.entrySet().stream()
+ .map(entry -> String.format("%s=%s", entry.getKey(), entry.getValue()))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/package-info.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/package-info.java
new file mode 100644
index 00000000..c0c646da
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 10:10
+ * @version: 1.0
+ */
+package cd.casic.module.execute.docker.dataobject;
\ No newline at end of file
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IContainerService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IContainerService.java
new file mode 100644
index 00000000..09f1a6a7
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IContainerService.java
@@ -0,0 +1,95 @@
+package cd.casic.module.execute.docker.service;
+
+import cd.casic.module.execute.docker.dataobject.model.RunNewContainer;
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.github.dockerjava.api.command.TopContainerResponse;
+import com.github.dockerjava.api.model.Container;
+
+import java.util.List;
+
+/**
+ * @description: 容器服务接口
+ * @author: mianbin
+ * @date: 2025/5/26 14:27
+ * @version: 1.0
+ */
+public interface IContainerService {
+ /**
+ * 查询容器列表,这个接口注意一下,showall参数 true 代表的是命令docker ps -a , false代表的是docker ps
+ *
+ * @param showAll
+ */
+ List list(String clientId, boolean showAll);
+
+ /**
+ * 运行新的容器
+ *
+ * @return
+ */
+ CreateContainerResponse run(String clientId, RunNewContainer runNewContainer);
+
+ /**
+ * 启动一个已经存在的容器
+ *
+ * @param containerId 容器ID
+ */
+ void start(String clientId, String containerId);
+
+ /**
+ * 停止容器
+ *
+ * @param containerId 停止的容器ID
+ */
+ void stop(String clientId, String containerId);
+
+ /**
+ * 暂停容器
+ *
+ * @apiNote 暂停指定容器(异步暂停)
+ */
+ void pause(String clientId, String containerId);
+
+ /**
+ * 取消暂停容器
+ *
+ * @param containerId 容器ID
+ * @apiNote 取消暂停容器
+ */
+ void unpause(String clientId, String containerId);
+
+ /**
+ * 移除指定容器
+ *
+ * @param containerId 容器ID
+ */
+ void remove(String clientId, String containerId);
+
+ /**
+ * 获取容器的资源使用情况
+ *
+ * @param containerId 容器ID
+ */
+ void rename(String clientId, String containerId, String newName);
+
+ /**
+ * 查询容器的线程信息,默认使用命令aux
+ *
+ * @param containerId 容器ID
+ * @return
+ */
+ TopContainerResponse top(String clientId, String containerId);
+
+ /**
+ * 查询容器详情
+ *
+ * @param containerId
+ * @return
+ */
+ InspectContainerResponse inspect(String clientId, String containerId);
+
+ /**
+ * 容器日志
+ */
+ void logs(String clientId, String containerId, String... cmd);
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IDockerPingService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IDockerPingService.java
new file mode 100644
index 00000000..fc424c92
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IDockerPingService.java
@@ -0,0 +1,14 @@
+package cd.casic.module.execute.docker.service;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 14:31
+ * @version: 1.0
+ */
+
+public interface IDockerPingService {
+
+ Boolean ping(String clientId);
+
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IImageService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IImageService.java
new file mode 100644
index 00000000..e6c774e0
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IImageService.java
@@ -0,0 +1,105 @@
+package cd.casic.module.execute.docker.service;
+
+import com.github.dockerjava.api.command.InspectImageResponse;
+import com.github.dockerjava.api.model.Image;
+import jakarta.annotation.Nonnull;
+
+import java.util.List;
+
+/**
+ * @description: 镜像相关服务
+ * @author: mianbin
+ * @date: 2025/5/26 14:28
+ * @version: 1.0
+ */
+public interface IImageService {
+ /**
+ * 获取镜像列表
+ *
+ * @param clientId 那个客户端
+ * @return
+ */
+ List list(String clientId);
+
+ /**
+ * 通过镜像ID获取镜像
+ *
+ * @param imageId
+ * @return
+ */
+ InspectImageResponse inspect(String clientId, String imageId);
+
+ /**
+ * 重新打tag。这个打tag注意
+ * hello-world hallo-newworld 74cc54e27dc4 4 months ago 10.1kB
+ * hello-world hallo-newworld1 74cc54e27dc4 4 months ago 10.1kB
+ * hello-world latest 74cc54e27dc4 4 months ago 10.1kB
+ * 类似与上面这样,但是你删除,docker rmi 74cc54e27dc4 ,那这三个全部gg
+ *
+ * @param imageId 镜像ID
+ * @param newTag 新的镜像Tag
+ */
+ void tag(@Nonnull String clientId, String imageId, String newTag);
+
+ /**
+ * 移除镜像
+ *
+ * @param imageId 镜像ID
+ * @param force
+ * @return
+ */
+ void remove(@Nonnull String clientId, String imageId, Boolean force);
+
+ /**
+ * 导出镜像
+ *
+ * @param imageId 镜像ID
+ */
+ void export(@Nonnull String clientId, String imageId);
+
+
+ /**
+ * 通过文件路径导入镜像导入镜像
+ *
+ * @param file 文件路径
+ */
+ void importByFile(@Nonnull String clientId, String file);
+
+ /**
+ * 通过Tar文件导入镜像
+ *
+ * @param file
+ */
+ void importByTar(@Nonnull String clientId, String file);
+
+ /**
+ * @param imageId
+ */
+ boolean exist(@Nonnull String clientId, String imageId);
+
+ /**
+ * 清理镜像
+ */
+ void pruneImage(@Nonnull String clientId);
+
+ /**
+ * 镜像保存
+ *
+ * @return
+ */
+ Boolean save(@Nonnull String clientId, String imageId, String outputPath);
+
+ /**
+ * 推送保存,如果你想推送镜像,需要在配置文件中修改镜像仓库
+ * 1: 如果你还想修改名字,或者鉴权,在这里修改
+ * dockerClient.pushImageCmd(imageId)
+ * .withTag("new-tag")
+ * .withname("new-name")
+ * .withAuthConfig(authConfig).exec();
+ * 2:如果做了以上操作,需要新增接口或者自己封装
+ *
+ * @return
+ */
+ Boolean pushImage(@Nonnull String clientId, String imageId);
+
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IMonitorService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IMonitorService.java
new file mode 100644
index 00000000..42b7b1f1
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IMonitorService.java
@@ -0,0 +1,11 @@
+package cd.casic.module.execute.docker.service;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 14:28
+ * @version: 1.0
+ */
+public interface IMonitorService {
+
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IOperateRecordService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IOperateRecordService.java
new file mode 100644
index 00000000..56004974
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IOperateRecordService.java
@@ -0,0 +1,16 @@
+package cd.casic.module.execute.docker.service;
+
+import cd.casic.module.execute.docker.dataobject.model.OperateRecord;
+
+/**
+ * @description: 操作日志查询
+ * @author: mianbin
+ * @date: 2025/5/26 16:46
+ * @version: 1.0
+ */
+public interface IOperateRecordService {
+ /**
+ * 新增操作日志
+ */
+ void add(OperateRecord record);
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ContainerService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ContainerService.java
new file mode 100644
index 00000000..8e477d93
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ContainerService.java
@@ -0,0 +1,165 @@
+package cd.casic.module.execute.docker.service.impl;
+
+import cd.casic.framework.commons.util.json.JsonUtils;
+import cd.casic.framework.security.core.util.SecurityFrameworkUtils;
+import cd.casic.module.execute.docker.DockerClientFactory;
+import cd.casic.module.execute.docker.callback.LoggingCallback;
+import cd.casic.module.execute.docker.dataobject.model.OperateRecord;
+import cd.casic.module.execute.docker.dataobject.model.RunNewContainer;
+import cd.casic.module.execute.docker.service.IContainerService;
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.async.ResultCallback;
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.github.dockerjava.api.command.TopContainerResponse;
+import com.github.dockerjava.api.model.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 16:34
+ * @version: 1.0
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ContainerService implements IContainerService {
+
+ private final DockerClientFactory dockerClientFactory;
+ private final OperateRecordService operateRecordService;
+
+ @Override
+ public List list(String clientId, boolean showAll) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ return dockerClient.listContainersCmd().withShowAll(showAll).exec();
+ }
+
+ @Override
+ public CreateContainerResponse run(String clientId, RunNewContainer runNewContainer) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ HostConfig hostConfig = HostConfig.newHostConfig();
+ hostConfig.withBinds(Bind.parse("/host:/container:ro"))
+ .withPortBindings(PortBinding.parse("12:44")).withLinks(Link.parse(""))
+ .withDns(runNewContainer.getDns()).withNetworkMode("网络名");
+ final CreateContainerResponse response = dockerClient
+ .createContainerCmd(runNewContainer.getImageId())
+ .withAliases(runNewContainer.getAlias())
+ .withHostConfig(hostConfig)
+ .withEnv(runNewContainer.findEnvList())
+ .withName(runNewContainer.getContainerName())
+ .withHostName(runNewContainer.getHostname()).exec();
+ return response;
+ }
+
+ @Override
+ public void start(String clientId, String containerId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ dockerClient.startContainerCmd(containerId).exec();
+ log.info("启动容器,containerId={}", containerId);
+ /*增加日志*/
+ extracted(containerId, "启动容器");
+ }
+
+ private void extracted(String containerId, String type) {
+ try {
+ Map param = new HashMap<>();
+ param.put("containerId", containerId);
+ param.put("type", type);
+ OperateRecord operateRecord = OperateRecord.builder()
+ .userId(SecurityFrameworkUtils.getLoginUserId())
+ /*这个是没有必要存放的*/
+ .clientId("")
+ .content(JsonUtils.toJsonString(param))
+ .name("未知")
+ .resource(OperateRecord.OperatorResource.CONTAINER_v1).build();
+ operateRecordService.add(operateRecord);
+ } catch (Exception ignore) {
+ //ignore exception
+ }
+ }
+
+ @Override
+ public void stop(String clientId, String containerId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ dockerClient.stopContainerCmd(containerId).exec();
+ log.info("停止容器,containerId={}", containerId);
+ /*增加日志*/
+ extracted(containerId, "停止容器");
+ }
+
+ @Override
+ public void pause(String clientId, String containerId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ dockerClient.pauseContainerCmd(containerId).exec();
+ log.info("暂停容器,containerId={}", containerId);
+ extracted(containerId, "暂停容器");
+ }
+
+ @Override
+ public void unpause(String clientId, String containerId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ dockerClient.unpauseContainerCmd(containerId).exec();
+ log.info("继续容器,containerId={}", containerId);
+ extracted(containerId, "恢复容器");
+ }
+
+ @Override
+ public void remove(String clientId, String containerId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ dockerClient.removeContainerCmd(containerId)
+ /*删除默认强制删除,并且挂载内容一并删除*/
+ .withForce(true)
+ .withRemoveVolumes(true).exec();
+ log.info("移除容器,containerId={}", containerId);
+ extracted(containerId, "移除容器");
+ }
+
+ @Override
+ public void rename(String clientId, String containerId, String newName) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ dockerClient.renameContainerCmd(containerId).withName(newName).exec();
+ log.info("重命名容器:[{}]为[{}]", containerId, newName);
+ extracted(containerId, "重命名容器");
+ }
+
+ @Override
+ public TopContainerResponse top(String clientId, String containerId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ return dockerClient.topContainerCmd(containerId).withPsArgs("aux").exec();
+ }
+
+ @Override
+ public InspectContainerResponse inspect(String clientId, String containerId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ final InspectContainerResponse response =
+ dockerClient.inspectContainerCmd(containerId).withSize(Boolean.TRUE).exec();
+ extracted(containerId, "Inspect容器");
+ return response;
+ }
+
+ @Override
+ public void logs(String clientId, String containerId, String... cmd) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ /*创建命令*/
+ String execId = dockerClient.execCreateCmd(containerId)
+ .withCmd(cmd)
+ .withAttachStdin(true)
+ .withAttachStdout(true)
+ .withAttachStderr(true)
+ .withTty(true)
+ .exec().getId();
+ // 执行命令并记录日志
+ ResultCallback callback = new LoggingCallback(containerId, execId);
+ /*这个是异步的方法*/
+ dockerClient.execStartCmd(execId)
+ .withTty(true)
+ .exec(callback);
+ }
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/DockerPingService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/DockerPingService.java
new file mode 100644
index 00000000..73821b8f
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/DockerPingService.java
@@ -0,0 +1,36 @@
+package cd.casic.module.execute.docker.service.impl;
+
+import cd.casic.module.execute.docker.DockerClientFactory;
+import cd.casic.module.execute.docker.service.IDockerPingService;
+import com.github.dockerjava.api.DockerClient;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.util.Locale;
+import java.util.Optional;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 14:32
+ * @version: 1.0
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DockerPingService implements IDockerPingService {
+
+ private final DockerClientFactory dockerClientFactory;
+
+ @Override
+ public Boolean ping(String clientId) {
+ DockerClient dockerClient = Optional.of(StringUtils.hasText(clientId) ? clientId : "DEFAULT")
+ .map(id -> id.toUpperCase(Locale.ROOT))
+ .map(dockerClientFactory::getdockerClient)
+ .orElseThrow(() -> new RuntimeException("客户端不存在"));
+ dockerClient.pingCmd().exec();
+ return Boolean.TRUE;
+ }
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java
new file mode 100644
index 00000000..dd65aa15
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java
@@ -0,0 +1,133 @@
+package cd.casic.module.execute.docker.service.impl;
+
+import cd.casic.module.execute.docker.DockerClientFactory;
+import cd.casic.module.execute.docker.service.IImageService;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.InspectImageResponse;
+import com.github.dockerjava.api.command.SaveImageCmd;
+import com.github.dockerjava.api.exception.NotFoundException;
+import com.github.dockerjava.api.model.Image;
+import com.github.dockerjava.api.model.PruneType;
+import jakarta.annotation.Nonnull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.stereotype.Service;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * @description: 镜像的服务类
+ * @author: mianbin
+ * @date: 2025/5/26 15:06
+ * @version: 1.0
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ImageService implements IImageService {
+
+ private final DockerClientFactory dockerClientFactory;
+
+ @Override
+ public List list(@Nonnull String clientId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ return dockerClient.listImagesCmd().exec();
+ }
+
+ @Override
+ public InspectImageResponse inspect(@Nonnull String clientId, @Nonnull String imageId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ return dockerClient.inspectImageCmd(imageId).exec();
+ }
+
+ @Override
+ public void tag(@Nonnull String clientId, String imageId, String newTag) {
+ InspectImageResponse inspect = inspect(clientId, imageId);
+ if (inspect == null) {
+ throw new RuntimeException("操作失败,镜像不存在");
+ }
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ dockerClient.tagImageCmd(imageId, inspect.getRepoTags().get(0), newTag).exec();
+ }
+
+ @Override
+ public void remove(@Nonnull String clientId, String imageId, Boolean force) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ dockerClient.removeImageCmd(imageId).withForce(force).exec();
+ }
+
+ @Override
+ public void export(@Nonnull String clientId, String imageId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ /*这个api我要查下,不生效也无所,感觉场景很少*/
+ }
+
+ /*经过测试,只有local 模式下面生效*/
+ @Override
+ public void importByFile(@Nonnull String clientId, String file) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ /*test 测试下,这个不知道生效不*/
+ dockerClient.loadImageCmd(FileUtil.getInputStream(file)).exec();
+ }
+
+ /*经过测试,只有local 模式下面生效*/
+ @Override
+ public void importByTar(@Nonnull String clientId, String file) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ /*test 测试下,这个不知道生效不*/
+ dockerClient.loadImageCmd(FileUtil.getInputStream(file)).exec();
+ }
+
+ @Override
+ public boolean exist(@Nonnull String clientId, String imageId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ try {
+ final InspectImageResponse inspectImage = dockerClient.inspectImageCmd(imageId).exec();
+ return inspectImage != null;
+ } catch (NotFoundException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void pruneImage(String clientId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ dockerClient.pruneCmd(PruneType.IMAGES).exec();
+ }
+
+ @Override
+ public Boolean save(@Nonnull String clientId, String imageId, String outputPath) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ /*imageid 如 nginx:latest */
+ try (OutputStream fos = new FileOutputStream(outputPath);
+ SaveImageCmd saveImageCmd = dockerClient.saveImageCmd(imageId)) {
+ // 执行命令并将输出写入文件流
+ InputStream exec = saveImageCmd.exec();
+ IoUtil.copy(exec, fos);
+ log.info("镜像已成功保存到: " + outputPath);
+ return Boolean.TRUE;
+ } catch (IOException e) {
+ log.error("保存镜像时发生 IO 错误: " + e.getMessage());
+ return Boolean.FALSE;
+ }
+ }
+
+ @Override
+ public Boolean pushImage(@NotNull String clientId, String imageId) {
+ DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
+ try {
+ dockerClient.pushImageCmd(imageId).exec(null);
+ } catch (Exception e) {
+ log.error("推送镜像失败: " + e.getMessage());
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/OperateRecordService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/OperateRecordService.java
new file mode 100644
index 00000000..77ec7087
--- /dev/null
+++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/OperateRecordService.java
@@ -0,0 +1,31 @@
+package cd.casic.module.execute.docker.service.impl;
+
+import cd.casic.module.execute.docker.dao.OperateRecordDao;
+import cd.casic.module.execute.docker.dataobject.model.OperateRecord;
+import cd.casic.module.execute.docker.service.IOperateRecordService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 16:47
+ * @version: 1.0
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OperateRecordService implements IOperateRecordService {
+
+ private final OperateRecordDao operateRecordDao;
+
+ @Override
+ public void add(OperateRecord record) {
+// OperateRecordDo operateRecordDo = OperateRecordConvert.INSTANCE.convert(record);
+// int insert = operateRecordDao.insert(operateRecordDo);
+// if (insert != 1) {
+// log.error("新增操作日志失败");
+// }
+ }
+}
diff --git a/modules/module-ci-execute/src/test/java/cd.casic.module.execute/ContainerServiceTest.java b/modules/module-ci-execute/src/test/java/cd.casic.module.execute/ContainerServiceTest.java
new file mode 100644
index 00000000..d082bf05
--- /dev/null
+++ b/modules/module-ci-execute/src/test/java/cd.casic.module.execute/ContainerServiceTest.java
@@ -0,0 +1,238 @@
+package cd.casic.module.execute;
+
+import cd.casic.module.execute.docker.callback.LoggingCallback;
+import cd.casic.module.execute.docker.dataobject.model.DockerEndpoint;
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.async.ResultCallback;
+import com.github.dockerjava.api.command.ExecCreateCmdResponse;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.github.dockerjava.api.command.TopContainerResponse;
+import com.github.dockerjava.api.exception.NotFoundException;
+import com.github.dockerjava.api.model.Container;
+import com.github.dockerjava.api.model.Frame;
+import com.github.dockerjava.core.DockerClientBuilder;
+import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static java.lang.String.format;
+import static java.lang.Thread.sleep;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 20:11
+ * @version: 1.0
+ */
+@Slf4j
+public class ContainerServiceTest {
+
+ private final Map clientGroup = new ConcurrentHashMap<>();
+
+ private ApacheDockerHttpClient createHttpClient(DockerEndpoint endpoint) {
+ try {
+ URI dockerHost;
+ if (endpoint.getType() == DockerEndpoint.DockerEndpointTypeEnum.LOCAL) {
+ // 使用本地挂载
+ dockerHost = new URI("unix:///var/run/docker.sock");
+ } else if (endpoint.getType() == DockerEndpoint.DockerEndpointTypeEnum.REMOTE) {
+ // 远程挂载
+ dockerHost = new URI(format("tcp://%s:%s", endpoint.getHost(), endpoint.getPort()));
+ } else {
+ log.error("Unsupported Docker endpoint type: {}", endpoint.getType());
+ return null;
+ }
+ return new ApacheDockerHttpClient.Builder()
+ .dockerHost(dockerHost)
+ .build();
+ } catch (URISyntaxException e) {
+ log.error("Failed to create URI for Docker endpoint {}: {}", endpoint.getId(), e.getMessage(), e);
+ return null;
+ }
+ }
+
+ @BeforeEach
+ public void setUp() {
+ DockerEndpoint endpoint = new DockerEndpoint();
+ endpoint.setId("158");
+ endpoint.setType(DockerEndpoint.DockerEndpointTypeEnum.REMOTE);
+ endpoint.setHost("175.6.27.158");
+ endpoint.setPort(22375);
+ endpoint.setName("test");
+ ApacheDockerHttpClient httpClient = createHttpClient(endpoint);
+ DockerClient dockerClient = DockerClientBuilder.getInstance().withDockerHttpClient(httpClient).build();
+ clientGroup.put(endpoint.getId(), dockerClient);
+ }
+
+ @Test
+ public void list() {
+ DockerClient dockerClient = clientGroup.get("158");
+ List exec =
+ dockerClient.listContainersCmd().withShowAll(true).exec();
+ System.out.println(exec.size());
+ }
+
+ @Test
+ public void start() {
+ String containerId = "d7d747696f43";
+ DockerClient dockerClient = clientGroup.get("158");
+ dockerClient.startContainerCmd(containerId).exec();
+ log.info("启动容器,containerId={}", containerId);
+ }
+
+ @Test
+ public void stop() {
+ String containerId = "d7d747696f43";
+ DockerClient dockerClient = clientGroup.get("158");
+ dockerClient.stopContainerCmd(containerId).exec();
+ log.info("停止容器,containerId={}", containerId);
+ }
+
+ /*没有成功,不知道为啥,暂停不研究了*/
+ @Test
+ public void pause() {
+ String containerId = "c1ffcaf2ff8d";
+ DockerClient dockerClient = clientGroup.get("158");
+ dockerClient.pauseContainerCmd(containerId).exec();
+ log.info("停止容器,containerId={}", containerId);
+ }
+
+ @Test
+ public void unpause() {
+ String containerId = "c1ffcaf2ff8d";
+ DockerClient dockerClient = clientGroup.get("158");
+ try {
+ dockerClient.unpauseContainerCmd(containerId).exec();
+ log.info("重启容器,containerId={}", containerId);
+ } catch (NotFoundException e) {
+ log.info("重启容器失败,containerId={},异常", containerId, e.getMessage());
+ }
+ }
+
+ @Test
+ public void rename() {
+ String containerId = "d7d747696f43";
+ String newName = "nginx";
+ DockerClient dockerClient = clientGroup.get("158");
+ dockerClient.renameContainerCmd(containerId).withName(newName).exec();
+ log.info("重命名容器:[{}]为[{}]", containerId, newName);
+ }
+
+ @Test
+ public void top() {
+ String containerId = "d7d747696f43";
+ String newName = "nginx";
+ DockerClient dockerClient = clientGroup.get("158");
+ TopContainerResponse aux = dockerClient.topContainerCmd(containerId).withPsArgs("aux").exec();
+ log.info("", aux.getTitles());
+ }
+
+ @Test
+ public void inspect() {
+ String containerId = "d7d747696f43";
+ DockerClient dockerClient = clientGroup.get("158");
+ final InspectContainerResponse response =
+ dockerClient.inspectContainerCmd(containerId).withSize(Boolean.TRUE).exec();
+ System.out.println(response);
+ }
+
+ @Test
+ public void remove() {
+ String containerId = "d7d747696f43";
+ DockerClient dockerClient = clientGroup.get("158");
+ dockerClient.removeContainerCmd(containerId)
+ /*删除默认强制删除,并且挂载内容一并删除*/
+ .withForce(true)
+ .withRemoveVolumes(true).exec();
+ log.info("移除容器,containerId={}", containerId);
+ }
+
+ /*这个测试,用处不大*/
+ @Test
+ public void setLog() throws InterruptedException {
+ String containerId = "d7d747696f43";
+ DockerClient dockerClient = clientGroup.get("158");
+ /*创建命令*/
+ String execId = dockerClient.execCreateCmd(containerId)
+ .withCmd("ls")
+ .withAttachStdin(true)
+ .withAttachStdout(true)
+ .withAttachStderr(true)
+ .withTty(true)
+ .exec().getId();
+ // 执行命令并记录日志
+ ResultCallback callback = new LoggingCallback(containerId, execId);
+ /*这个是异步的方法*/
+ dockerClient.execStartCmd(execId)
+ .withTty(true)
+ .exec(callback);
+ sleep(1000);
+ }
+
+ @Test
+ public void execCmd() throws InterruptedException {
+ String containerId = "c1ffcaf2ff8d22ae9f1d9a91d0714d086ec2812809aec1558c9512b04168c125";
+ DockerClient dockerClient = clientGroup.get("158");
+ ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
+ .withAttachStdout(true)
+ .withAttachStderr(true)
+ .withCmd("ls", "-l")
+ .exec();
+ dockerClient
+ .execStartCmd(execCreateCmdResponse.getId())
+ .exec(new CustomExecCallback())
+ .awaitCompletion();
+ }
+
+ /**
+ * 自定义执行命令回调类,用于处理命令执行过程中的输出和错误信息。
+ */
+ private static class CustomExecCallback extends ResultCallback.Adapter {
+
+ @Override
+ public void onNext(Frame frame) {
+ String output = new String(frame.getPayload(), StandardCharsets.UTF_8);
+ switch (frame.getStreamType()) {
+ case STDOUT:
+ System.out.print(output);
+// log.info("标准输出: {}", output.trim());
+ break;
+ case STDERR:
+ System.err.print(output);
+ log.error("错误输出: {}", output.trim());
+ break;
+ default:
+ log.warn("未知流类型: {}", frame.getStreamType());
+ }
+ super.onNext(frame);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ log.error("执行命令时出错: {}", throwable.getMessage(), throwable);
+ super.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ log.info("命令执行完毕");
+ super.onComplete();
+ }
+
+ @Override
+ public void close() throws IOException {
+ log.debug("回调已关闭");
+ super.close();
+ }
+ }
+
+}
diff --git a/modules/module-ci-execute/src/test/java/cd.casic.module.execute/DockerPingService.java b/modules/module-ci-execute/src/test/java/cd.casic.module.execute/DockerPingService.java
new file mode 100644
index 00000000..2b30898b
--- /dev/null
+++ b/modules/module-ci-execute/src/test/java/cd.casic.module.execute/DockerPingService.java
@@ -0,0 +1,76 @@
+package cd.casic.module.execute;
+
+import cd.casic.module.execute.docker.dataobject.model.DockerEndpoint;
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.core.DockerClientBuilder;
+import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.util.StringUtils;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static java.lang.String.format;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 20:11
+ * @version: 1.0
+ */
+@Slf4j
+public class DockerPingService {
+ private final Map clientGroup = new ConcurrentHashMap<>();
+
+ private ApacheDockerHttpClient createHttpClient(DockerEndpoint endpoint) {
+ try {
+ URI dockerHost;
+ if (endpoint.getType() == DockerEndpoint.DockerEndpointTypeEnum.LOCAL) {
+ // 使用本地挂载
+ dockerHost = new URI("unix:///var/run/docker.sock");
+ } else if (endpoint.getType() == DockerEndpoint.DockerEndpointTypeEnum.REMOTE) {
+ // 远程挂载
+ dockerHost = new URI(format("tcp://%s:%s", endpoint.getHost(), endpoint.getPort()));
+ } else {
+ log.error("Unsupported Docker endpoint type: {}", endpoint.getType());
+ return null;
+ }
+ return new ApacheDockerHttpClient.Builder()
+ .dockerHost(dockerHost)
+ .build();
+ } catch (URISyntaxException e) {
+ log.error("Failed to create URI for Docker endpoint {}: {}", endpoint.getId(), e.getMessage(), e);
+ return null;
+ }
+ }
+
+ @BeforeEach
+ public void setUp() {
+ DockerEndpoint endpoint = new DockerEndpoint();
+ endpoint.setId("158");
+ endpoint.setType(DockerEndpoint.DockerEndpointTypeEnum.REMOTE);
+ endpoint.setHost("175.6.27.158");
+ endpoint.setPort(22375);
+ endpoint.setName("test");
+ ApacheDockerHttpClient httpClient = createHttpClient(endpoint);
+ DockerClient dockerClient = DockerClientBuilder.getInstance().withDockerHttpClient(httpClient).build();
+ clientGroup.put(endpoint.getId(), dockerClient);
+ }
+
+ @Test
+ public void ping(){
+ String clientId = "158";
+ DockerClient dockerClient = Optional.of(StringUtils.hasText(clientId) ? clientId : "DEFAULT")
+ .map(id -> id.toUpperCase(Locale.ROOT))
+ .map(t->clientGroup.get("158"))
+ .orElseThrow(() -> new RuntimeException("客户端不存在"));
+ dockerClient.pingCmd().exec();
+ }
+
+}
diff --git a/modules/module-ci-execute/src/test/java/cd.casic.module.execute/ImageServiceTest.java b/modules/module-ci-execute/src/test/java/cd.casic.module.execute/ImageServiceTest.java
new file mode 100644
index 00000000..7580e8f8
--- /dev/null
+++ b/modules/module-ci-execute/src/test/java/cd.casic.module.execute/ImageServiceTest.java
@@ -0,0 +1,130 @@
+package cd.casic.module.execute;
+
+import cd.casic.module.execute.docker.dataobject.model.DockerEndpoint;
+import cn.hutool.core.io.FileUtil;
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.InspectImageResponse;
+import com.github.dockerjava.api.model.Image;
+import com.github.dockerjava.core.DockerClientBuilder;
+import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static java.lang.String.format;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 20:11
+ * @version: 1.0
+ */
+@Slf4j
+public class ImageServiceTest {
+ private final Map clientGroup = new ConcurrentHashMap<>();
+
+ private ApacheDockerHttpClient createHttpClient(DockerEndpoint endpoint) {
+ try {
+ URI dockerHost;
+ if (endpoint.getType() == DockerEndpoint.DockerEndpointTypeEnum.LOCAL) {
+ // 使用本地挂载
+ dockerHost = new URI("unix:///var/run/docker.sock");
+ } else if (endpoint.getType() == DockerEndpoint.DockerEndpointTypeEnum.REMOTE) {
+ // 远程挂载
+ dockerHost = new URI(format("tcp://%s:%s", endpoint.getHost(), endpoint.getPort()));
+ } else {
+ log.error("Unsupported Docker endpoint type: {}", endpoint.getType());
+ return null;
+ }
+ return new ApacheDockerHttpClient.Builder()
+ .dockerHost(dockerHost)
+ .build();
+ } catch (URISyntaxException e) {
+ log.error("Failed to create URI for Docker endpoint {}: {}", endpoint.getId(), e.getMessage(), e);
+ return null;
+ }
+ }
+
+ @BeforeEach
+ public void setUp() {
+ DockerEndpoint endpoint = new DockerEndpoint();
+ endpoint.setId("158");
+ endpoint.setType(DockerEndpoint.DockerEndpointTypeEnum.REMOTE);
+ endpoint.setHost("175.6.27.158");
+ endpoint.setPort(22375);
+ endpoint.setName("test");
+ ApacheDockerHttpClient httpClient = createHttpClient(endpoint);
+ DockerClient dockerClient = DockerClientBuilder.getInstance().withDockerHttpClient(httpClient).build();
+ clientGroup.put(endpoint.getId(), dockerClient);
+ }
+
+ @Test
+ public void listTest() {
+ DockerClient dockerClient = clientGroup.get("158");
+ List exec = dockerClient.listImagesCmd().exec();
+ System.out.println(exec);
+ }
+
+ @Test
+ public void inspect() {
+ DockerClient dockerClient = clientGroup.get("158");
+ InspectImageResponse exec = dockerClient
+ .inspectImageCmd("5a9e3ee3b8c1be74f5f89b092aea791d3ca058054ec0f3522b79d9985eff3087").exec();
+ System.out.println(exec);
+ }
+
+ @Test
+ public void tag() {
+ /* 测试的hallo-world */
+ String imagesId = "74cc54e27dc4";
+ DockerClient dockerClient = clientGroup.get("158");
+ InspectImageResponse inspect = dockerClient
+ .inspectImageCmd(imagesId).exec();
+ if (inspect == null) {
+ throw new RuntimeException("操作失败,镜像不存在");
+ }
+ dockerClient.tagImageCmd(imagesId, inspect.getRepoTags().get(0), "hallo-newworld1").exec();
+ }
+
+ @Test
+ public void remove() {
+ String imageId = "be69f2940aaf";
+ DockerClient dockerClient = clientGroup.get("158");
+ dockerClient.removeImageCmd(imageId).withForce(true).exec();
+ }
+
+ /*todo local 模式生效*/
+ @Test
+ public void importByTar() {
+ DockerClient dockerClient = clientGroup.get("158");
+ File file = new File("/home/ubuntu/nginx.tar");
+ Void exec = dockerClient.loadImageCmd(FileUtil.getInputStream(file)).exec();
+ }
+
+ @Test
+ public void exist() {
+ String imageId = "be69f2940aaf";
+ DockerClient dockerClient = clientGroup.get("158");
+ System.out.println(dockerClient.inspectImageCmd(imageId).exec() != null);
+ }
+
+ @Test
+ public void pushImage() {
+ String imageId = "be69f2940aaf";
+ DockerClient dockerClient = clientGroup.get("158");
+ try {
+ dockerClient.pushImageCmd(imageId).exec(null);
+ log.info("推送镜像成功: ");
+ } catch (Exception e) {
+ log.error("推送镜像失败: " + e.getMessage());
+ }
+ }
+
+}
diff --git a/modules/module-ci-execute/src/test/java/cd.casic.module.execute/MonitorService.java b/modules/module-ci-execute/src/test/java/cd.casic.module.execute/MonitorService.java
new file mode 100644
index 00000000..a90a264b
--- /dev/null
+++ b/modules/module-ci-execute/src/test/java/cd.casic.module.execute/MonitorService.java
@@ -0,0 +1,11 @@
+package cd.casic.module.execute;
+
+/**
+ * @description: TODO
+ * @author: mianbin
+ * @date: 2025/5/26 20:11
+ * @version: 1.0
+ */
+public class MonitorService {
+
+}