[REMOVE] Remove DockerPullPushService, DockerRegistryController, and DockerRegistryService due to deprecation
parent
81b351360f
commit
2488de4aad
@ -1,59 +0,0 @@
|
|||||||
package kr.re.etri.autoflow.controllers;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import kr.re.etri.autoflow.entity.PullRequest;
|
|
||||||
import kr.re.etri.autoflow.payload.request.TagWithDigest;
|
|
||||||
import kr.re.etri.autoflow.service.DockerPullPushService;
|
|
||||||
import kr.re.etri.autoflow.service.DockerRegistryService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/docker")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class DockerRegistryController {
|
|
||||||
|
|
||||||
private final DockerRegistryService dockerRegistryService;
|
|
||||||
private final DockerPullPushService dockerPullPushService;
|
|
||||||
|
|
||||||
@GetMapping("/repositories")
|
|
||||||
@Operation(summary = "모든 리포지토리 조회")
|
|
||||||
public Mono<ResponseEntity<?>> listRepositories() {
|
|
||||||
return dockerRegistryService.listRepositories()
|
|
||||||
.map(ResponseEntity::ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/repositories/search")
|
|
||||||
@Operation(summary = "리포지토리 검색 (키워드 + 페이지네이션)")
|
|
||||||
public Mono<ResponseEntity<?>> searchRepositories(
|
|
||||||
@RequestParam(required = false) String keyword,
|
|
||||||
@RequestParam(defaultValue = "0") int page,
|
|
||||||
@RequestParam(defaultValue = "10") int size) {
|
|
||||||
return dockerRegistryService.searchRepositories(keyword, page, size)
|
|
||||||
.map(ResponseEntity::ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/repositories/{repo}/tags")
|
|
||||||
@Operation(summary = "특정 리포지토리의 태그 조회")
|
|
||||||
public Mono<ResponseEntity<?>> listTags(@PathVariable String repo) {
|
|
||||||
return dockerRegistryService.listTags(repo)
|
|
||||||
.map(ResponseEntity::ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "이미지 pull (manifest + config + blobs) 후 로컬에 저장")
|
|
||||||
@PostMapping("/pull")
|
|
||||||
public Mono<ResponseEntity<Map<String, Object>>> pullImage(@RequestBody PullRequest req) {
|
|
||||||
return dockerPullPushService.pullImage(req.getRegistry(), req.getRepository(), req.getTag(), req.getUsername(), req.getPassword())
|
|
||||||
.map(result -> ResponseEntity.ok(Map.of(
|
|
||||||
"savedFiles", result,
|
|
||||||
"path", dockerPullPushService.getDownloadBaseDir()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
package kr.re.etri.autoflow.service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.util.FileSystemUtils;
|
|
||||||
import org.springframework.web.reactive.function.BodyExtractors;
|
|
||||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
import reactor.netty.http.client.HttpClient;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class DockerPullPushService {
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
@Value("${docker-registry.download-dir:./downloads}")
|
|
||||||
private String downloadBaseDir;
|
|
||||||
|
|
||||||
public String getDownloadBaseDir() {
|
|
||||||
return downloadBaseDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Mono<List<String>> pullImage(String registry, String repository, String tag, String username, String password) {
|
|
||||||
if (registry == null || registry.isBlank()) {
|
|
||||||
registry = "localhost:5000"; // 기본값
|
|
||||||
}
|
|
||||||
|
|
||||||
String base = registry.startsWith("http") ? registry : "http://" + registry;
|
|
||||||
WebClient.Builder builder = WebClient.builder()
|
|
||||||
.clientConnector(new ReactorClientHttpConnector(HttpClient.create().responseTimeout(Duration.ofSeconds(60))))
|
|
||||||
.baseUrl(base);
|
|
||||||
|
|
||||||
if (username != null && !username.isBlank()) {
|
|
||||||
builder.defaultHeaders(h -> h.setBasicAuth(username, password == null ? "" : password));
|
|
||||||
}
|
|
||||||
|
|
||||||
WebClient client = builder.build();
|
|
||||||
|
|
||||||
Path dstDir = Path.of(downloadBaseDir, repository.replace('/', '-'), tag);
|
|
||||||
try {
|
|
||||||
if (Files.exists(dstDir)) {
|
|
||||||
FileSystemUtils.deleteRecursively(dstDir);
|
|
||||||
}
|
|
||||||
Files.createDirectories(dstDir);
|
|
||||||
} catch (IOException e) {
|
|
||||||
return Mono.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1) manifest
|
|
||||||
String manifestAccept = "application/vnd.docker.distribution.manifest.v2+json";
|
|
||||||
return client.get()
|
|
||||||
.uri(uriBuilder -> uriBuilder.path("/v2/{repo}/manifests/{tag}").build(repository, tag))
|
|
||||||
.header(HttpHeaders.ACCEPT, manifestAccept)
|
|
||||||
.accept(MediaType.valueOf("application/vnd.docker.distribution.manifest.v2+json"))
|
|
||||||
.exchangeToMono(response -> saveResponseBodyToFile(response, dstDir.resolve("image.manifest")))
|
|
||||||
.flatMap(manifestPath -> {
|
|
||||||
try {
|
|
||||||
byte[] b = Files.readAllBytes(manifestPath);
|
|
||||||
JsonNode manifest = objectMapper.readTree(b);
|
|
||||||
List<String> saved = new ArrayList<>();
|
|
||||||
saved.add(manifestPath.toString());
|
|
||||||
|
|
||||||
// 2) config (config is a blob)
|
|
||||||
JsonNode config = manifest.get("config");
|
|
||||||
Mono<Void> configMono = Mono.empty();
|
|
||||||
if (config != null && config.has("digest")) {
|
|
||||||
String configDigest = config.get("digest").asText();
|
|
||||||
Path configPath = dstDir.resolve(configDigest);
|
|
||||||
configMono = client.get()
|
|
||||||
.uri(uriBuilder -> uriBuilder.path("/v2/{repo}/blobs/{digest}").build(repository, configDigest))
|
|
||||||
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
|
||||||
.exchangeToMono(response -> saveResponseBodyToFile(response, configPath).then())
|
|
||||||
.doOnSuccess(v -> saved.add(configPath.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) layers
|
|
||||||
JsonNode layers = manifest.get("layers");
|
|
||||||
Flux<Void> layersFlux = Flux.empty();
|
|
||||||
if (layers != null && layers.isArray()) {
|
|
||||||
List<Mono<Void>> monos = new ArrayList<>();
|
|
||||||
for (JsonNode layer : layers) {
|
|
||||||
String digest = layer.get("digest").asText();
|
|
||||||
Path layerPath = dstDir.resolve(digest);
|
|
||||||
Mono<Void> m = client.get()
|
|
||||||
.uri(uriBuilder -> uriBuilder.path("/v2/{repo}/blobs/{digest}").build(repository, digest))
|
|
||||||
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
|
||||||
.exchangeToMono(response -> saveResponseBodyToFile(response, layerPath).then())
|
|
||||||
.doOnSuccess(v -> saved.add(layerPath.toString()));
|
|
||||||
monos.add(m);
|
|
||||||
}
|
|
||||||
layersFlux = Flux.concat(monos);
|
|
||||||
}
|
|
||||||
|
|
||||||
return configMono.thenMany(layersFlux).then(Mono.just(saved));
|
|
||||||
} catch (IOException e) {
|
|
||||||
return Mono.error(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<Path> saveResponseBodyToFile(ClientResponse response, Path path) {
|
|
||||||
if (response.statusCode().is2xxSuccessful()) {
|
|
||||||
return response.body(BodyExtractors.toDataBuffers())
|
|
||||||
.map(dataBuffer -> {
|
|
||||||
try (var in = dataBuffer.asInputStream(true)) {
|
|
||||||
Files.copy(in, path);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
})
|
|
||||||
.next()
|
|
||||||
.onErrorMap(t -> t);
|
|
||||||
} else {
|
|
||||||
return response.createException().flatMap(Mono::error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
package kr.re.etri.autoflow.service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import kr.re.etri.autoflow.payload.request.TagWithDigest;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
import java.util.stream.StreamSupport;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class DockerRegistryService {
|
|
||||||
|
|
||||||
private final WebClient.Builder webClientBuilder;
|
|
||||||
private static final String REGISTRY_URL = "http://192.168.10.135:5000";
|
|
||||||
|
|
||||||
public Mono<JsonNode> listRepositories() {
|
|
||||||
return webClientBuilder.build()
|
|
||||||
.get()
|
|
||||||
.uri(REGISTRY_URL + "/v2/_catalog")
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(JsonNode.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검색 + 페이지네이션
|
|
||||||
public Mono<List<String>> searchRepositories(String keyword, int page, int size) {
|
|
||||||
return webClientBuilder.build()
|
|
||||||
.get()
|
|
||||||
.uri(REGISTRY_URL + "/v2/_catalog")
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(JsonNode.class)
|
|
||||||
.map(json -> {
|
|
||||||
JsonNode repositoriesNode = json.get("repositories");
|
|
||||||
if (repositoriesNode == null || !repositoriesNode.isArray()) {
|
|
||||||
return List.<String>of();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> allRepos = StreamSupport.stream(repositoriesNode.spliterator(), false)
|
|
||||||
.map(JsonNode::asText)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 키워드 필터링
|
|
||||||
if (keyword != null && !keyword.isBlank()) {
|
|
||||||
allRepos = allRepos.stream()
|
|
||||||
.filter(name -> name.contains(keyword))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 페이지네이션
|
|
||||||
int fromIndex = Math.min(page * size, allRepos.size());
|
|
||||||
int toIndex = Math.min(fromIndex + size, allRepos.size());
|
|
||||||
return allRepos.subList(fromIndex, toIndex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Mono<JsonNode> listTags(String repository) {
|
|
||||||
return webClientBuilder.build()
|
|
||||||
.get()
|
|
||||||
.uri(REGISTRY_URL + "/v2/" + repository + "/tags/list")
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(JsonNode.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in new issue