|
@@ -0,0 +1,440 @@
|
|
|
|
|
+package space.anyi.docker;
|
|
|
|
|
+
|
|
|
|
|
+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.CreateImageResponse;
|
|
|
|
|
+import com.github.dockerjava.api.command.LogContainerCmd;
|
|
|
|
|
+import com.github.dockerjava.api.command.TopContainerResponse;
|
|
|
|
|
+import com.github.dockerjava.api.model.ChangeLog;
|
|
|
|
|
+import com.github.dockerjava.api.model.Frame;
|
|
|
|
|
+import com.github.dockerjava.api.model.PruneResponse;
|
|
|
|
|
+import com.github.dockerjava.api.model.PruneType;
|
|
|
|
|
+import com.github.dockerjava.api.model.Statistics;
|
|
|
|
|
+import com.github.dockerjava.api.model.StreamType;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.FileOutputStream;
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.io.InputStream;
|
|
|
|
|
+import java.nio.file.Files;
|
|
|
|
|
+import java.nio.file.Path;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.concurrent.atomic.AtomicReference;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Docker 日常运维与维护 API 示例
|
|
|
|
|
+ * 演示监控查看(stats/top/logs -f)、文件系统变更(diff)、归档(export/import/commit)、
|
|
|
|
|
+ * 生命周期补全(restart/pause/rename/update)、复制文件进容器(cp -进)等操作
|
|
|
|
|
+ * 每个操作旁标注了等价的 Docker CLI 命令,方便对照理解
|
|
|
|
|
+ */
|
|
|
|
|
+public class MaintenanceOpsAPI {
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(MaintenanceOpsAPI.class);
|
|
|
|
|
+
|
|
|
|
|
+ private final DockerClient dockerClient;
|
|
|
|
|
+
|
|
|
|
|
+ public MaintenanceOpsAPI() {
|
|
|
|
|
+ this.dockerClient = DockerClientFactory.createDockerClient();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 生命周期补全 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 重启容器(先停止马上再启动)
|
|
|
|
|
+ * 等价命令: docker restart <containerId>
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ */
|
|
|
|
|
+ public void restartContainer(String containerId) {
|
|
|
|
|
+ // 等价命令: docker restart <containerId>(内部 = stop + start)
|
|
|
|
|
+ dockerClient.restartContainerCmd(containerId).exec();
|
|
|
|
|
+ log.info("容器 {} 已重启", containerId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 暂停容器(冻结进程,不停止)
|
|
|
|
|
+ * 等价命令: docker pause <containerId>
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ */
|
|
|
|
|
+ public void pauseContainer(String containerId) {
|
|
|
|
|
+ // 等价命令: docker pause <containerId>(基于 cgroup freezer,进程被冻结)
|
|
|
|
|
+ dockerClient.pauseContainerCmd(containerId).exec();
|
|
|
|
|
+ log.info("容器 {} 已暂停", containerId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 恢复已暂停的容器
|
|
|
|
|
+ * 等价命令: docker unpause <containerId>
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ */
|
|
|
|
|
+ public void unpauseContainer(String containerId) {
|
|
|
|
|
+ // 等价命令: docker unpause <containerId>
|
|
|
|
|
+ dockerClient.unpauseContainerCmd(containerId).exec();
|
|
|
|
|
+ log.info("容器 {} 已恢复", containerId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 重命名容器(容器在运行中也可改名)
|
|
|
|
|
+ * 等价命令: docker rename <containerId> <newName>
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ * @param newName 新名称
|
|
|
|
|
+ */
|
|
|
|
|
+ public void renameContainer(String containerId, String newName) {
|
|
|
|
|
+ // 等价命令: docker rename <containerId> <newName>
|
|
|
|
|
+ dockerClient.renameContainerCmd(containerId).withName(newName).exec();
|
|
|
|
|
+ log.info("容器 {} 已重命名为 {}", containerId, newName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 动态更新容器资源限制(无需重建容器)
|
|
|
|
|
+ * 等价命令: docker update --memory 256m <containerId>
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ * @param memoryMb 新的内存限制(MB)
|
|
|
|
|
+ */
|
|
|
|
|
+ public void updateContainerResources(String containerId, long memoryMb) {
|
|
|
|
|
+ // 等价命令: docker update --memory 256m <containerId>
|
|
|
|
|
+ // 注意:Docker 要求 memory-swap >= memory,只改 memory 会返回 409,
|
|
|
|
|
+ // 因此这里同步调整 memory-swap(保持为内存的 2 倍,与 docker 默认一致)
|
|
|
|
|
+ long memoryBytes = memoryMb * 1024 * 1024;
|
|
|
|
|
+ dockerClient.updateContainerCmd(containerId)
|
|
|
|
|
+ .withMemory(memoryBytes)
|
|
|
|
|
+ .withMemorySwap(memoryBytes * 2)
|
|
|
|
|
+ .exec();
|
|
|
|
|
+ log.info("容器 {} 资源限制已更新为 {}MB", containerId, memoryMb);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 监控与查看 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查看容器内运行的进程(等价 docker top)
|
|
|
|
|
+ * 等价命令: docker top <containerId>
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ * @return 进程列表(每行一个 String[])
|
|
|
|
|
+ */
|
|
|
|
|
+ public String[][] topContainer(String containerId) {
|
|
|
|
|
+ // 等价命令: docker top <containerId>(相当于容器内 ps aux)
|
|
|
|
|
+ TopContainerResponse response = dockerClient.topContainerCmd(containerId).exec();
|
|
|
|
|
+ var titles = response.getTitles();
|
|
|
|
|
+ log.info("容器 {} 进程列表 (字段: {})", containerId, String.join(", ", titles));
|
|
|
|
|
+ for (String[] process : response.getProcesses()) {
|
|
|
|
|
+ log.info(" {}", String.join(" ", process));
|
|
|
|
|
+ }
|
|
|
|
|
+ return response.getProcesses();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取容器资源使用统计(单次快照,等价 docker stats --no-stream)
|
|
|
|
|
+ * 等价命令: docker stats --no-stream <containerId>
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ * @return 统计信息
|
|
|
|
|
+ */
|
|
|
|
|
+ public Statistics getContainerStats(String containerId) {
|
|
|
|
|
+ // 单次快照:withNoStream(true) 只取一帧就完成,不持续输出
|
|
|
|
|
+ // 等价命令: docker stats --no-stream <containerId>
|
|
|
|
|
+ AtomicReference<Statistics> snapshot = new AtomicReference<>();
|
|
|
|
|
+ try {
|
|
|
|
|
+ dockerClient.statsCmd(containerId)
|
|
|
|
|
+ .withNoStream(true)
|
|
|
|
|
+ .exec(new ResultCallback.Adapter<>() {
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onNext(Statistics stats) {
|
|
|
|
|
+ snapshot.set(stats);
|
|
|
|
|
+ }
|
|
|
|
|
+ }).awaitCompletion();
|
|
|
|
|
+
|
|
|
|
|
+ Statistics stats = snapshot.get();
|
|
|
|
|
+ if (stats != null && stats.getMemoryStats() != null) {
|
|
|
|
|
+ long usageMb = stats.getMemoryStats().getUsage() / 1024 / 1024;
|
|
|
|
|
+ long limitMb = stats.getMemoryStats().getLimit() / 1024 / 1024;
|
|
|
|
|
+ log.info("容器 {} 内存使用: {}MB / {}MB, 进程数: {}",
|
|
|
|
|
+ containerId, usageMb, limitMb, stats.getNumProcs());
|
|
|
|
|
+ }
|
|
|
|
|
+ return stats;
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ log.error("获取容器统计被中断: {}", e.getMessage());
|
|
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 实时跟随容器日志(等价 docker logs -f)
|
|
|
|
|
+ * 与 Ch6 的 getContainerLogs(一次性读取)不同,这里持续输出新日志直到超时
|
|
|
|
|
+ * 等价命令: docker logs -f --tail 50 <containerId>
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ * @param followSeconds 跟随时长(秒),超过后自动断开
|
|
|
|
|
+ * @return 跟随期间收集到的日志
|
|
|
|
|
+ */
|
|
|
|
|
+ public String followContainerLogs(String containerId, long followSeconds) {
|
|
|
|
|
+ // 等价命令: docker logs -f <containerId>
|
|
|
|
|
+ // withFollowStream(true) 进入跟随模式,容器产生新日志会实时推送到回调
|
|
|
|
|
+ StringBuilder output = new StringBuilder();
|
|
|
|
|
+ try {
|
|
|
|
|
+ LogContainerCmd cmd = dockerClient.logContainerCmd(containerId)
|
|
|
|
|
+ .withStdOut(true)
|
|
|
|
|
+ .withStdErr(true)
|
|
|
|
|
+ .withFollowStream(true)
|
|
|
|
|
+ .withTail(50);
|
|
|
|
|
+
|
|
|
|
|
+ ResultCallback<Frame> callback = cmd.exec(new ResultCallback.Adapter<>() {
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onNext(Frame frame) {
|
|
|
|
|
+ if (frame.getStreamType() == StreamType.STDOUT ||
|
|
|
|
|
+ frame.getStreamType() == StreamType.STDERR ||
|
|
|
|
|
+ frame.getStreamType() == StreamType.RAW) {
|
|
|
|
|
+ output.append(new String(frame.getPayload()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 跟随指定时长后主动断开(模拟 Ctrl+C 结束 docker logs -f)
|
|
|
|
|
+ Thread.sleep(followSeconds * 1000);
|
|
|
|
|
+ callback.close();
|
|
|
|
|
+
|
|
|
|
|
+ log.info("跟随 {} 秒后收集日志 {} 字节", followSeconds, output.length());
|
|
|
|
|
+ return output.toString();
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ log.error("跟随日志被中断: {}", e.getMessage());
|
|
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
|
|
+ return output.toString();
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ log.error("关闭日志流失败: {}", e.getMessage());
|
|
|
|
|
+ return output.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 文件系统 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查看容器相对镜像的文件系统变更(新增/修改/删除)
|
|
|
|
|
+ * 等价命令: docker diff <containerId>
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ * @return 变更列表
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<ChangeLog> containerDiff(String containerId) {
|
|
|
|
|
+ // 等价命令: docker diff <containerId>
|
|
|
|
|
+ // kind: 0=已修改 1=新增 2=删除
|
|
|
|
|
+ List<ChangeLog> changes = dockerClient.containerDiffCmd(containerId).exec();
|
|
|
|
|
+ for (ChangeLog change : changes) {
|
|
|
|
|
+ log.info(" {} {}", change.getKind() == 1 ? "新增" :
|
|
|
|
|
+ change.getKind() == 2 ? "删除" : "修改", change.getPath());
|
|
|
|
|
+ }
|
|
|
|
|
+ return changes;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 复制本地文件/目录到容器中(docker cp 的放入方向)
|
|
|
|
|
+ * 等价命令: docker cp ./local.txt <containerId>:/tmp/local.txt
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ * @param hostResource 宿主机文件或目录路径
|
|
|
|
|
+ * @param containerPath 容器内目标路径
|
|
|
|
|
+ */
|
|
|
|
|
+ public void copyToContainer(String containerId, String hostResource, String containerPath) {
|
|
|
|
|
+ // 等价命令: docker cp ./local.txt <containerId>:/tmp/local.txt
|
|
|
|
|
+ dockerClient.copyArchiveToContainerCmd(containerId)
|
|
|
|
|
+ .withHostResource(hostResource)
|
|
|
|
|
+ .withRemotePath(containerPath)
|
|
|
|
|
+ .exec();
|
|
|
|
|
+ log.info("已复制 {} 到容器 {} 的 {}", hostResource, containerId, containerPath);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 归档与快照 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将运行中容器的文件系统导出为 tar 文件(不含镜像层和元数据)
|
|
|
|
|
+ * 等价命令: docker export <containerId> -o backup.tar
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ * @param tarFilePath 导出文件路径
|
|
|
|
|
+ * @throws IOException 文件写入失败
|
|
|
|
|
+ */
|
|
|
|
|
+ public void exportContainer(String containerId, String tarFilePath) throws IOException {
|
|
|
|
|
+ // 等价命令: docker export <containerId> -o backup.tar
|
|
|
|
|
+ // 导出的是容器当前文件系统的快照,可配合 docker import 恢复为镜像
|
|
|
|
|
+ try (InputStream tarStream = dockerClient.exportContainerCmd(containerId).exec();
|
|
|
|
|
+ FileOutputStream fos = new FileOutputStream(tarFilePath)) {
|
|
|
|
|
+ tarStream.transferTo(fos);
|
|
|
|
|
+ log.info("容器 {} 文件系统已导出到 {}", containerId, tarFilePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将 tar 文件导入为镜像(docker export 的逆操作)
|
|
|
|
|
+ * 等价命令: docker import backup.tar my-app:v2
|
|
|
|
|
+ * @param tarFilePath 容器导出的 tar 文件
|
|
|
|
|
+ * @param repository 目标仓库名
|
|
|
|
|
+ * @param tag 目标标签
|
|
|
|
|
+ * @return 是否成功
|
|
|
|
|
+ */
|
|
|
|
|
+ public boolean importImageFromTar(String tarFilePath, String repository, String tag) {
|
|
|
|
|
+ // 等价命令: docker import backup.tar my-app:v2
|
|
|
|
|
+ // createImageCmd(name, stream) 用于从 tar 流导入镜像
|
|
|
|
|
+ try (InputStream tarStream = Files.newInputStream(Path.of(tarFilePath))) {
|
|
|
|
|
+ // createImageCmd 是同步命令,exec() 直接阻塞直到导入完成
|
|
|
|
|
+ CreateImageResponse response = dockerClient.createImageCmd(repository, tarStream)
|
|
|
|
|
+ .withTag(tag)
|
|
|
|
|
+ .exec();
|
|
|
|
|
+ log.info("镜像 {}:{} 导入成功,ID: {}", repository, tag, response.getId());
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ log.error("镜像导入失败: {}", e.getMessage());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将容器的当前状态保存为新镜像(docker commit)
|
|
|
|
|
+ * 与 docker export 不同,commit 会保留镜像层历史、配置和元数据
|
|
|
|
|
+ * 等价命令: docker commit -m "init app" <containerId> my-app:v3
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ * @param repository 目标仓库名
|
|
|
|
|
+ * @param tag 目标标签
|
|
|
|
|
+ * @return 新镜像 ID
|
|
|
|
|
+ */
|
|
|
|
|
+ public String commitContainerToImage(String containerId, String repository, String tag) {
|
|
|
|
|
+ // 等价命令: docker commit <containerId> my-app:v3
|
|
|
|
|
+ String imageId = dockerClient.commitCmd(containerId)
|
|
|
|
|
+ .withRepository(repository)
|
|
|
|
|
+ .withTag(tag)
|
|
|
|
|
+ .withMessage("committed by docker-java")
|
|
|
|
|
+ .exec();
|
|
|
|
|
+ log.info("容器 {} 已提交为镜像 {}:{},镜像 ID: {}", containerId, repository, tag, imageId);
|
|
|
|
|
+ return imageId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 清理(谨慎使用) ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 清理未使用的数据(docker 磁盘清理)
|
|
|
|
|
+ * 注意:这是"危险"操作,会删除所有未使用的资源!
|
|
|
|
|
+ * 等价命令:
|
|
|
|
|
+ * docker container prune -f (删除已停止的容器)
|
|
|
|
|
+ * docker image prune -f (删除悬空镜像)
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>教程仅作 API 演示,请勿在测试中执行以免误删环境数据</p>
|
|
|
|
|
+ * @return 清理结果摘要
|
|
|
|
|
+ */
|
|
|
|
|
+ public String pruneUnusedResources() {
|
|
|
|
|
+ // 等价命令: docker container prune -f:只清理已停止的容器
|
|
|
|
|
+ PruneResponse containers = dockerClient.pruneCmd(PruneType.CONTAINERS).exec();
|
|
|
|
|
+ // 等价命令: docker image prune -f:只清理悬空(dangling)镜像
|
|
|
|
|
+ PruneResponse images = dockerClient.pruneCmd(PruneType.IMAGES).exec();
|
|
|
|
|
+ log.info("容器清理: 回收 {} 字节, 镜像清理: 回收 {} 字节",
|
|
|
|
|
+ containers.getSpaceReclaimed(), images.getSpaceReclaimed());
|
|
|
|
|
+ return "containerReclaimed=" + containers.getSpaceReclaimed()
|
|
|
|
|
+ + ", imageReclaimed=" + images.getSpaceReclaimed();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 测试辅助 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查看容器详情(测试辅助,等价 docker inspect)
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ * @return 容器详情
|
|
|
|
|
+ */
|
|
|
|
|
+ public com.github.dockerjava.api.command.InspectContainerResponse inspectContainer(String containerId) {
|
|
|
|
|
+ return dockerClient.inspectContainerCmd(containerId).exec();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 停止容器(测试辅助,导出文件系统前先停止容器以避免快照不一致)
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ */
|
|
|
|
|
+ public void stopContainer(String containerId) {
|
|
|
|
|
+ dockerClient.stopContainerCmd(containerId).withTimeout(5).exec();
|
|
|
|
|
+ log.info("容器 {} 已停止", containerId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建可持续运行的临时容器(sleep 保持存活,供监控/复制等操作使用)
|
|
|
|
|
+ * @param containerName 容器名称
|
|
|
|
|
+ * @return 容器 ID
|
|
|
|
|
+ */
|
|
|
|
|
+ public String createSleepingContainer(String containerName) {
|
|
|
|
|
+ // 等价命令: docker run -d --name tmp-app amazoncorretto:17 sleep 300
|
|
|
|
|
+ CreateContainerResponse container = dockerClient.createContainerCmd("amazoncorretto:17")
|
|
|
|
|
+ .withName(containerName)
|
|
|
|
|
+ .withEntrypoint("sh", "-c")
|
|
|
|
|
+ .withCmd("sleep 300")
|
|
|
|
|
+ .exec();
|
|
|
|
|
+ dockerClient.startContainerCmd(container.getId()).exec();
|
|
|
|
|
+ return container.getId();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建并启动一个执行指定命令的临时容器(测试辅助)
|
|
|
|
|
+ * 等价命令: docker run -d --name tmp-app amazoncorretto:17 sh -c "<command>"
|
|
|
|
|
+ * @param containerName 容器名称
|
|
|
|
|
+ * @param command 由 sh -c 执行的命令
|
|
|
|
|
+ * @return 容器 ID
|
|
|
|
|
+ */
|
|
|
|
|
+ public String createContainer(String containerName, String... command) {
|
|
|
|
|
+ CreateContainerResponse container = dockerClient.createContainerCmd("amazoncorretto:17")
|
|
|
|
|
+ .withName(containerName)
|
|
|
|
|
+ .withEntrypoint("sh", "-c")
|
|
|
|
|
+ .withCmd(command)
|
|
|
|
|
+ .exec();
|
|
|
|
|
+ dockerClient.startContainerCmd(container.getId()).exec();
|
|
|
|
|
+ return container.getId();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 删除容器(测试清理用)
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ */
|
|
|
|
|
+ public void removeContainer(String containerId) {
|
|
|
|
|
+ dockerClient.removeContainerCmd(containerId).withForce(true).exec();
|
|
|
|
|
+ log.info("容器 {} 已删除", containerId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 删除镜像(测试清理用)
|
|
|
|
|
+ * @param image 镜像 ID 或名称
|
|
|
|
|
+ */
|
|
|
|
|
+ public void removeImage(String image) {
|
|
|
|
|
+ dockerClient.removeImageCmd(image).withForce(true).exec();
|
|
|
|
|
+ log.info("镜像 {} 已删除", image);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 在容器内执行命令并返回输出(测试辅助,等价 docker exec)
|
|
|
|
|
+ * @param containerId 容器 ID
|
|
|
|
|
+ * @param command 命令参数
|
|
|
|
|
+ * @return 输出文本
|
|
|
|
|
+ */
|
|
|
|
|
+ public String execInContainer(String containerId, String... command) {
|
|
|
|
|
+ StringBuilder output = new StringBuilder();
|
|
|
|
|
+ try {
|
|
|
|
|
+ var exec = dockerClient.execCreateCmd(containerId)
|
|
|
|
|
+ .withCmd(command)
|
|
|
|
|
+ .withAttachStdout(true)
|
|
|
|
|
+ .withAttachStderr(true)
|
|
|
|
|
+ .exec();
|
|
|
|
|
+ dockerClient.execStartCmd(exec.getId())
|
|
|
|
|
+ .exec(new ResultCallback.Adapter<>() {
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onNext(Frame frame) {
|
|
|
|
|
+ if (frame.getStreamType() == StreamType.STDOUT ||
|
|
|
|
|
+ frame.getStreamType() == StreamType.STDERR) {
|
|
|
|
|
+ output.append(new String(frame.getPayload()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }).awaitCompletion();
|
|
|
|
|
+ return output.toString();
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ log.error("容器内执行命令被中断: {}", e.getMessage());
|
|
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
|
|
+ return output.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查镜像是否存在(测试辅助)
|
|
|
|
|
+ * @param image 镜像名称(仓库:标签)
|
|
|
|
|
+ * @return 是否存在
|
|
|
|
|
+ */
|
|
|
|
|
+ public boolean checkImageExists(String image) {
|
|
|
|
|
+ return dockerClient.listImagesCmd().exec().stream()
|
|
|
|
|
+ .anyMatch(img -> img.getRepoTags() != null &&
|
|
|
|
|
+ java.util.Arrays.asList(img.getRepoTags()).contains(image));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|