RegistryPushAPITest.java 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. package space.anyi.docker;
  2. import org.junit.jupiter.api.Test;
  3. import java.io.IOException;
  4. import java.net.ServerSocket;
  5. import java.net.URI;
  6. import java.net.http.HttpClient;
  7. import java.net.http.HttpRequest;
  8. import java.net.http.HttpResponse;
  9. import java.time.Duration;
  10. import java.util.UUID;
  11. import static org.junit.jupiter.api.Assertions.*;
  12. /**
  13. * 镜像推送与 Registry 认证 API 的测试类
  14. * 注意:需要本机 Docker daemon 运行中(会自动拉取 registry:2 镜像)
  15. * 测试会启动一个本地私有仓库,结束后自动清理
  16. */
  17. class RegistryPushAPITest {
  18. private String uniqueName(String prefix) {
  19. return prefix + "-" + UUID.randomUUID().toString().substring(0, 8);
  20. }
  21. /**
  22. * 动态获取一个空闲端口,避免与其他进程冲突
  23. */
  24. private int freePort() throws IOException {
  25. try (ServerSocket socket = new ServerSocket(0)) {
  26. return socket.getLocalPort();
  27. }
  28. }
  29. /**
  30. * 测试 docker login:对本地私有仓库认证应返回 Login Succeeded
  31. */
  32. @Test
  33. void authToRegistryReturnsLoginSucceeded() throws IOException {
  34. RegistryPushAPI api = new RegistryPushAPI();
  35. int port = freePort();
  36. String containerId = api.startLocalRegistry(uniqueName("reg"), port);
  37. String registryAddress = "localhost:" + port;
  38. String status = api.authToRegistry(registryAddress);
  39. assertEquals("Login Succeeded", status, "未启用认证的私有仓库应返回 Login Succeeded");
  40. api.stopAndRemoveRegistryContainer(containerId);
  41. }
  42. /**
  43. * 测试完整闭环:启动仓库 -> 打标签 -> 推送 -> 验证仓库有镜像 -> 删本地 -> 拉回
  44. */
  45. @Test
  46. void pushThenPullRoundTrip() throws IOException, InterruptedException {
  47. RegistryPushAPI api = new RegistryPushAPI();
  48. int port = freePort();
  49. String registryContainerId = api.startLocalRegistry(uniqueName("reg"), port);
  50. String repository = uniqueName("myapp");
  51. String tag = "v1";
  52. String registryAddress = "localhost:" + port;
  53. String remoteImage = registryAddress + "/" + repository + ":" + tag;
  54. try {
  55. // 1. 认证
  56. assertEquals("Login Succeeded", api.authToRegistry(registryAddress));
  57. // 2. 打标签并推送
  58. boolean pushed = api.tagAndPushImage("nginx:latest", registryAddress, repository, tag);
  59. assertTrue(pushed, "镜像应推送成功");
  60. // 3. 通过 Registry HTTP API 验证仓库中确实存在该镜像
  61. // 等价命令: curl http://localhost:5000/v2/_catalog
  62. HttpResponse<String> catalogResponse = queryRegistry(port);
  63. String catalogBody = catalogResponse.body();
  64. assertTrue(catalogBody.contains(repository),
  65. "仓库目录中应包含推送的镜像,实际: " + catalogBody);
  66. // 4. 删除本地镜像副本(模拟镜像仅存在于仓库)
  67. api.removeImage(remoteImage);
  68. assertFalse(api.checkImageExists(remoteImage), "本地镜像应已被删除");
  69. // 5. 从仓库拉回并验证
  70. boolean pulled = api.pullFromRegistry(registryAddress, repository, tag);
  71. assertTrue(pulled, "镜像应能从仓库拉回");
  72. assertTrue(api.checkImageExists(remoteImage), "拉回后本地应存在该镜像");
  73. } finally {
  74. api.removeImage(remoteImage);
  75. api.stopAndRemoveRegistryContainer(registryContainerId);
  76. }
  77. }
  78. /**
  79. * 查询本地仓库目录(Registry HTTP API v2)
  80. * 等价命令: curl http://localhost:<port>/v2/_catalog
  81. */
  82. private HttpResponse<String> queryRegistry(int port) throws IOException, InterruptedException {
  83. HttpClient client = HttpClient.newBuilder()
  84. .connectTimeout(Duration.ofSeconds(3))
  85. .build();
  86. HttpRequest request = HttpRequest.newBuilder(
  87. URI.create("http://localhost:" + port + "/v2/_catalog"))
  88. .GET()
  89. .build();
  90. HttpResponse<String> response = null;
  91. for (int i = 0; i < 10; i++) {
  92. try {
  93. response = client.send(request, HttpResponse.BodyHandlers.ofString());
  94. break;
  95. } catch (IOException | InterruptedException e) {
  96. Thread.sleep(1000);
  97. }
  98. }
  99. assertNotNull(response, "本地仓库未能及时响应");
  100. return response;
  101. }
  102. }