|
|
@@ -0,0 +1,199 @@
|
|
|
+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.ExecCreateCmdResponse;
|
|
|
+import com.github.dockerjava.api.command.LogContainerCmd;
|
|
|
+import com.github.dockerjava.api.command.WaitContainerResultCallback;
|
|
|
+import com.github.dockerjava.api.model.Frame;
|
|
|
+import com.github.dockerjava.api.model.StreamType;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Docker 容器运维 API 示例
|
|
|
+ * 演示获取日志、执行命令、复制文件、等待容器等功能
|
|
|
+ */
|
|
|
+public class ContainerOpsAPI {
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(ContainerOpsAPI.class);
|
|
|
+
|
|
|
+ private final DockerClient dockerClient;
|
|
|
+
|
|
|
+ public ContainerOpsAPI() {
|
|
|
+ this.dockerClient = DockerClientFactory.createDockerClient();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取容器日志(同步方式)
|
|
|
+ * 只获取标准输出,stdout=true, stderr=false
|
|
|
+ * @param containerId 容器 ID
|
|
|
+ * @param tailLines 只显示末尾的行数
|
|
|
+ * @return 日志文本
|
|
|
+ */
|
|
|
+ public String getContainerLogs(String containerId, int tailLines) {
|
|
|
+ StringBuilder logBuilder = new StringBuilder();
|
|
|
+ try {
|
|
|
+ // 使用日志容器回调收集日志
|
|
|
+ // withTail 只获取末尾 N 行
|
|
|
+ LogContainerCmd cmd = dockerClient.logContainerCmd(containerId)
|
|
|
+ .withStdOut(true) // 获取标准输出
|
|
|
+ .withStdErr(true) // 获取错误输出
|
|
|
+ .withTail(tailLines); // 只取末尾几行
|
|
|
+
|
|
|
+ cmd.exec(new ResultCallback.Adapter<>() {
|
|
|
+ @Override
|
|
|
+ public void onNext(Frame frame) {
|
|
|
+ // 日志以帧(Frame)形式返回,不同帧包含不同类型的数据
|
|
|
+ if (frame.getStreamType() == StreamType.STDOUT ||
|
|
|
+ frame.getStreamType() == StreamType.STDERR ||
|
|
|
+ frame.getStreamType() == StreamType.RAW) {
|
|
|
+ logBuilder.append(new String(frame.getPayload()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }).awaitCompletion();
|
|
|
+ return logBuilder.toString();
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ log.error("获取容器日志被中断: {}", e.getMessage());
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ return logBuilder.toString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在容器中执行命令(同步方式)
|
|
|
+ * @param containerId 容器 ID
|
|
|
+ * @param command 要执行的命令参数,如 ["ls", "-la"] 或 ["echo", "hello"]
|
|
|
+ * @return 命令输出的文本
|
|
|
+ */
|
|
|
+ public String execCommandInContainer(String containerId, String... command) {
|
|
|
+ StringBuilder output = new StringBuilder();
|
|
|
+ try {
|
|
|
+ // 1. 创建 exec 实例(描述要在容器中执行什么命令)
|
|
|
+ ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
|
|
|
+ .withCmd(command) // 要执行的命令
|
|
|
+ .withAttachStdout(true) // 挂接标准输出
|
|
|
+ .withAttachStderr(true) // 挂接错误输出
|
|
|
+ .exec();
|
|
|
+
|
|
|
+ // 2. 启动 exec(真正执行命令,并接收输出)
|
|
|
+ dockerClient.execStartCmd(execCreateCmdResponse.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 containerId 容器 ID
|
|
|
+ * @param containerPath 容器内文件路径
|
|
|
+ * @param localDest 宿主机目标目录
|
|
|
+ */
|
|
|
+ public void copyFromContainer(String containerId, String containerPath, String localDest) {
|
|
|
+ try {
|
|
|
+ // 从容器复制文件(tar 流形式返回)
|
|
|
+ try (var stream = dockerClient.copyArchiveFromContainerCmd(containerId, containerPath)
|
|
|
+ .withHostPath(localDest)
|
|
|
+ .exec()) {
|
|
|
+ // 文件将通过 tar 流复制到目标路径
|
|
|
+ }
|
|
|
+ log.info("已从容器 {} 复制 {} 到 {}", containerId, containerPath, localDest);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("复制文件失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 等待容器退出
|
|
|
+ * @param containerId 容器 ID
|
|
|
+ * @param timeoutSeconds 超时时间(秒)
|
|
|
+ * @return 容器退出码(-1 表示超时)
|
|
|
+ */
|
|
|
+ public int waitContainer(String containerId, int timeoutSeconds) {
|
|
|
+ try {
|
|
|
+ WaitContainerResultCallback callback = dockerClient.waitContainerCmd(containerId)
|
|
|
+ .exec(new WaitContainerResultCallback());
|
|
|
+ // 阻塞等待容器退出,设置超时
|
|
|
+ int exitCode = callback.awaitStatusCode(timeoutSeconds, TimeUnit.SECONDS);
|
|
|
+ log.info("容器 {} 已退出,退出码: {}", containerId, exitCode);
|
|
|
+ return exitCode;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("等待容器退出异常: {}", e.getMessage());
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 为运维演示创建临时容器
|
|
|
+ * 注意:halo 镜像自带 ENTRYPOINT,需要覆盖 entrypoint 才能让自定义命令生效
|
|
|
+ * @param containerName 容器名称
|
|
|
+ * @param shellCommand 要执行的 shell 命令(单个字符串,将被 "sh -c" 包装执行)
|
|
|
+ * @return 容器 ID
|
|
|
+ */
|
|
|
+ public String createTemporaryContainer(String containerName, String shellCommand) {
|
|
|
+ // withEntrypoint("sh", "-c") 覆盖镜像自带的 ENTRYPOINT
|
|
|
+ // withCmd(shellCommand) 作为 sh -c 的唯一参数(即要执行的脚本内容)
|
|
|
+ CreateContainerResponse container = dockerClient.createContainerCmd("registry.fit2cloud.com/halo/halo-pro:2.24")
|
|
|
+ .withName(containerName)
|
|
|
+ .withEntrypoint("sh", "-c")
|
|
|
+ .withCmd(shellCommand)
|
|
|
+ .exec();
|
|
|
+ dockerClient.startContainerCmd(container.getId()).exec();
|
|
|
+ return container.getId();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除容器(测试清理用)
|
|
|
+ * @param containerId 容器 ID
|
|
|
+ * @param force 是否强制删除
|
|
|
+ * @param removeVolumes 是否同时删除数据卷
|
|
|
+ */
|
|
|
+ public void removeContainer(String containerId, boolean force, boolean removeVolumes) {
|
|
|
+ dockerClient.removeContainerCmd(containerId)
|
|
|
+ .withForce(force)
|
|
|
+ .withRemoveVolumes(removeVolumes)
|
|
|
+ .exec();
|
|
|
+ log.info("容器 {} 已删除", containerId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取最新运行的容器的示例日志(演示用)
|
|
|
+ * 简化版:创建容器、执行命令、删除容器
|
|
|
+ */
|
|
|
+ public void opsDemo() {
|
|
|
+ String containerName = "ops-demo-" + System.currentTimeMillis();
|
|
|
+ try {
|
|
|
+ // 1. 创建一个执行 echo 命令的容器
|
|
|
+ String id = createTemporaryContainer(containerName, "echo HelloOps && sleep 999");
|
|
|
+ log.info("1. 临时容器已创建并启动: {}", id);
|
|
|
+
|
|
|
+ // 2. 在容器中执行命令
|
|
|
+ String result = execCommandInContainer(id, "ls", "-la", "/");
|
|
|
+ log.info("2. 容器内 ls -la / 输出:\n{}", result);
|
|
|
+
|
|
|
+ // 3. 获取容器日志
|
|
|
+ String logs = getContainerLogs(id, 10);
|
|
|
+ log.info("3. 容器日志:\n{}", logs);
|
|
|
+
|
|
|
+ // 4. 清理容器
|
|
|
+ removeContainer(id, true, false);
|
|
|
+ log.info("4. 临时容器已清理");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("运维演示失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|