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 { + +}