|
|
@@ -0,0 +1,216 @@
|
|
|
+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.PullImageResultCallback;
|
|
|
+import com.github.dockerjava.api.model.AuthConfig;
|
|
|
+import com.github.dockerjava.api.model.AuthResponse;
|
|
|
+import com.github.dockerjava.api.model.ExposedPort;
|
|
|
+import com.github.dockerjava.api.model.HostConfig;
|
|
|
+import com.github.dockerjava.api.model.Ports;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+import java.util.Arrays;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Docker 镜像推送与 Registry 认证 API 示例
|
|
|
+ * 以本地 registry 演示 docker login / docker tag / docker push / docker pull 的完整闭环
|
|
|
+ * 每个操作旁标注了等价的 Docker CLI 命令,方便对照理解
|
|
|
+ */
|
|
|
+public class RegistryPushAPI {
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(RegistryPushAPI.class);
|
|
|
+
|
|
|
+ private final DockerClient dockerClient;
|
|
|
+
|
|
|
+ /** 本地私有仓库镜像 */
|
|
|
+ public static final String REGISTRY_IMAGE = "registry:2";
|
|
|
+
|
|
|
+ public RegistryPushAPI() {
|
|
|
+ this.dockerClient = DockerClientFactory.createDockerClient();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 启动一个本地私有仓库容器(registry:2)
|
|
|
+ * 等价命令:
|
|
|
+ * docker run -d --name my-registry -p 5000:5000 registry:2
|
|
|
+ * @param containerName 容器名称
|
|
|
+ * @param hostPort 宿主机端口(映射到仓库的 5000 端口)
|
|
|
+ * @return 容器 ID
|
|
|
+ */
|
|
|
+ public String startLocalRegistry(String containerName, int hostPort) {
|
|
|
+ // 等价命令: docker run -d --name my-registry -p 5000:5000 registry:2
|
|
|
+ ExposedPort registryPort = ExposedPort.tcp(5000);
|
|
|
+ Ports portBindings = new Ports();
|
|
|
+ portBindings.bind(registryPort, Ports.Binding.bindPort(hostPort));
|
|
|
+
|
|
|
+ HostConfig hostConfig = HostConfig.newHostConfig().withPortBindings(portBindings);
|
|
|
+
|
|
|
+ CreateContainerResponse container = dockerClient.createContainerCmd(REGISTRY_IMAGE)
|
|
|
+ .withName(containerName)
|
|
|
+ .withExposedPorts(registryPort)
|
|
|
+ .withHostConfig(hostConfig)
|
|
|
+ .exec();
|
|
|
+ dockerClient.startContainerCmd(container.getId()).exec();
|
|
|
+ log.info("本地私有仓库已启动: http://localhost:{}/", hostPort);
|
|
|
+ return container.getId();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 向私有仓库进行认证(等价 docker login)
|
|
|
+ * 本地 registry 默认未开启认证,注册任意凭据都会返回 Login Succeeded
|
|
|
+ * 等价命令: docker login localhost:5000
|
|
|
+ * @param registryAddress 仓库地址,如 "localhost:5000"
|
|
|
+ * @return 认证结果状态
|
|
|
+ */
|
|
|
+ public String authToRegistry(String registryAddress) {
|
|
|
+ // 等价命令: docker login localhost:5000
|
|
|
+ // 真实场景下(如 Docker Hub)需要提供有效的用户名/密码,
|
|
|
+ // 本地私有仓库默认不校验,任意凭据即可
|
|
|
+ AuthConfig authConfig = new AuthConfig()
|
|
|
+ .withUsername("docker-java-demo")
|
|
|
+ .withPassword("demo-password")
|
|
|
+ .withRegistryAddress(registryAddress);
|
|
|
+
|
|
|
+ AuthResponse response = dockerClient.authCmd()
|
|
|
+ .withAuthConfig(authConfig)
|
|
|
+ .exec();
|
|
|
+ log.info("Registry {} 认证结果: {}", registryAddress, response.getStatus());
|
|
|
+ return response.getStatus();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将本地镜像打标签后推送到私有仓库
|
|
|
+ * 等价命令:
|
|
|
+ * docker tag nginx:latest localhost:5000/my-nginx:v1
|
|
|
+ * docker push localhost:5000/my-nginx:v1
|
|
|
+ * @param sourceImage 源镜像(本地已有),如 "nginx:latest"
|
|
|
+ * @param registryAddress 仓库地址,如 "localhost:5000"
|
|
|
+ * @param repository 仓库名,如 "my-nginx"
|
|
|
+ * @param tag 标签,如 "v1"
|
|
|
+ * @return 推送是否成功
|
|
|
+ */
|
|
|
+ public boolean tagAndPushImage(String sourceImage, String registryAddress,
|
|
|
+ String repository, String tag) {
|
|
|
+ String remoteImage = registryAddress + "/" + repository;
|
|
|
+
|
|
|
+ // 等价命令: docker tag nginx:latest localhost:5000/my-nginx:v1
|
|
|
+ dockerClient.tagImageCmd(sourceImage, remoteImage, tag).exec();
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 等价命令: docker push localhost:5000/my-nginx:v1
|
|
|
+ // 3.7.1 没有 PushImageResultCallback,用通用回调 Adapter 接收推送进度
|
|
|
+ // withTag 指定要推送的标签;awaitCompletion() 等待推送完成
|
|
|
+ dockerClient.pushImageCmd(remoteImage)
|
|
|
+ .withTag(tag)
|
|
|
+ .exec(new ResultCallback.Adapter<>())
|
|
|
+ .awaitCompletion();
|
|
|
+ log.info("镜像 {}:{} 推送成功", remoteImage, tag);
|
|
|
+ return true;
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ log.error("镜像推送被中断: {}", e.getMessage());
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从私有仓库拉取镜像(验证推送结果的逆向操作)
|
|
|
+ * 等价命令: docker pull localhost:5000/my-nginx:v1
|
|
|
+ * @param registryAddress 仓库地址
|
|
|
+ * @param repository 仓库名
|
|
|
+ * @param tag 标签
|
|
|
+ * @return 拉取是否成功
|
|
|
+ */
|
|
|
+ public boolean pullFromRegistry(String registryAddress, String repository, String tag) {
|
|
|
+ String image = registryAddress + "/" + repository + ":" + tag;
|
|
|
+ try {
|
|
|
+ // 等价命令: docker pull localhost:5000/my-nginx:v1
|
|
|
+ dockerClient.pullImageCmd(image)
|
|
|
+ .exec(new PullImageResultCallback())
|
|
|
+ .awaitCompletion();
|
|
|
+ log.info("镜像 {} 从仓库拉取成功", image);
|
|
|
+ return true;
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ log.error("镜像拉取被中断: {}", e.getMessage());
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除本地镜像(测试清理用)
|
|
|
+ * 等价命令: docker rmi <image>
|
|
|
+ * @param image 镜像名称
|
|
|
+ */
|
|
|
+ public void removeImage(String image) {
|
|
|
+ dockerClient.removeImageCmd(image).withForce(true).exec();
|
|
|
+ log.info("镜像 {} 已删除", image);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查本地是否存在指定镜像
|
|
|
+ * @param image 镜像名称(仓库:标签)
|
|
|
+ * @return 是否存在
|
|
|
+ */
|
|
|
+ public boolean checkImageExists(String image) {
|
|
|
+ // 等价命令: docker images | grep <image>
|
|
|
+ return dockerClient.listImagesCmd().exec().stream()
|
|
|
+ .anyMatch(img -> img.getRepoTags() != null &&
|
|
|
+ Arrays.asList(img.getRepoTags()).contains(image));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 停止并删除 registry 容器(测试清理用)
|
|
|
+ * 等价命令: docker rm -f <containerId>
|
|
|
+ * @param containerId 容器 ID
|
|
|
+ */
|
|
|
+ public void stopAndRemoveRegistryContainer(String containerId) {
|
|
|
+ dockerClient.removeContainerCmd(containerId)
|
|
|
+ .withForce(true)
|
|
|
+ .withRemoveVolumes(false)
|
|
|
+ .exec();
|
|
|
+ log.info("本地私有仓库容器 {} 已停止并删除", containerId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 完整演示:启动仓库 -> 认证 -> 打标签 -> 推送 -> 删除本地 -> 重新拉取
|
|
|
+ * @param containerName registry 容器名
|
|
|
+ * @param hostPort registry 宿主机端口
|
|
|
+ * @return 完整流程是否成功
|
|
|
+ */
|
|
|
+ public boolean fullRegistryRoundTrip(String containerName, int hostPort, String repository, String tag) {
|
|
|
+ String registryAddress = "localhost:" + hostPort;
|
|
|
+ String remoteImage = registryAddress + "/" + repository + ":" + tag;
|
|
|
+ try {
|
|
|
+ // 1. 启动本地私有仓库
|
|
|
+ String registryContainerId = startLocalRegistry(containerName, hostPort);
|
|
|
+
|
|
|
+ // 2. docker login
|
|
|
+ String status = authToRegistry(registryAddress);
|
|
|
+ log.info("认证状态: {}", status);
|
|
|
+
|
|
|
+ // 3. docker tag + docker push
|
|
|
+ boolean pushed = tagAndPushImage("nginx:latest", registryAddress, repository, tag);
|
|
|
+ if (!pushed) return false;
|
|
|
+
|
|
|
+ // 4. 删除本地副本,模拟"仓库是唯一来源"
|
|
|
+ if (checkImageExists(remoteImage)) {
|
|
|
+ removeImage(remoteImage);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 从仓库重新拉取,验证闭环
|
|
|
+ boolean pulled = pullFromRegistry(registryAddress, repository, tag);
|
|
|
+ log.info("完整推送-拉取闭环结果: {}", pulled);
|
|
|
+
|
|
|
+ // 清理
|
|
|
+ removeImage(remoteImage);
|
|
|
+ stopAndRemoveRegistryContainer(registryContainerId);
|
|
|
+ return pulled;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("Registry 完整流程失败: {}", e.getMessage());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|