[ADD] Docker Registry 컨트롤러 및 서비스 추가, 이미지 업로드/다운로드 등 API 구현

main
bjkim 9 months ago
parent 01f92b25bc
commit 0c22c832e0

@ -0,0 +1,93 @@
package kr.re.etri.autoflow.controllers;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Operation;
import kr.re.etri.autoflow.payload.request.TagWithDigest;
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;
@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);
}
@GetMapping("/repositories/{repo}/tags-with-digest")
@Operation(summary = "태그 목록 + digest 조회")
public Mono<ResponseEntity<List<TagWithDigest>>> listTagsWithDigest(
@PathVariable String repo) {
return dockerRegistryService.listTagsWithDigest(repo)
.map(ResponseEntity::ok);
}
@PostMapping(value = "/repositories/{repo}/upload", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@Operation(summary = "이미지 업로드")
public Mono<ResponseEntity<String>> uploadImage(@PathVariable String repo,
@RequestParam String tag,
@RequestBody byte[] content) {
return dockerRegistryService.uploadImage(repo, tag, content)
.map(ResponseEntity::ok);
}
@GetMapping("/repositories/{repo}/download/{digest}")
@Operation(summary = "이미지 다운로드")
public Mono<ResponseEntity<byte[]>> downloadImage(@PathVariable String repo,
@PathVariable String digest) {
return dockerRegistryService.downloadImage(repo, digest)
.map(bytes -> ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes));
}
@GetMapping("/repositories/{repo}/manifest/{tag}")
@Operation(summary = "태그로 manifest 조회")
public Mono<ResponseEntity<JsonNode>> getManifest(
@PathVariable String repo,
@PathVariable String tag) {
return dockerRegistryService.getManifestByTag(repo, tag)
.map(ResponseEntity::ok);
}
@GetMapping("/repositories/{repo}/digest/{tag}")
@Operation(summary = "태그로 digest 조회")
public Mono<ResponseEntity<String>> getDigest(
@PathVariable String repo,
@PathVariable String tag) {
return dockerRegistryService.getDigestByTag(repo, tag)
.map(ResponseEntity::ok);
}
}

@ -0,0 +1,11 @@
package kr.re.etri.autoflow.payload.request;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class TagWithDigest {
private String tag;
private String digest;
}

@ -0,0 +1,131 @@
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);
}
public Mono<List<TagWithDigest>> listTagsWithDigest(String repository) {
return listTags(repository)
.flatMapMany(json -> {
JsonNode tagsNode = json.get("tags");
if (tagsNode == null || !tagsNode.isArray()) {
return Flux.empty();
}
return Flux.fromIterable(
StreamSupport.stream(tagsNode.spliterator(), false)
.map(JsonNode::asText)
.toList()
);
})
.flatMap(tag -> getDigestByTag(repository, tag)
.map(digest -> new TagWithDigest(tag, digest))
)
.collectList();
}
public Mono<String> uploadImage(String repository, String tag, byte[] content) {
return webClientBuilder.build()
.put()
.uri(REGISTRY_URL + "/v2/" + repository + "/blobs/uploads/")
.header(HttpHeaders.CONTENT_TYPE, "application/octet-stream")
.bodyValue(content)
.retrieve()
.bodyToMono(String.class);
}
public Mono<byte[]> downloadImage(String repository, String digest) {
return webClientBuilder.build()
.get()
.uri(REGISTRY_URL + "/v2/" + repository + "/blobs/" + digest)
.retrieve()
.bodyToMono(byte[].class);
}
public Mono<JsonNode> getManifestByTag(String repository, String tag) {
return webClientBuilder.build()
.get()
.uri(REGISTRY_URL + "/v2/" + repository + "/manifests/" + tag)
.header("Accept", "application/vnd.oci.image.index.v1+json")
.retrieve()
.bodyToMono(JsonNode.class);
}
// SHA(Digest)값만 가져옴
public Mono<String> getDigestByTag(String repository, String tag) {
return webClientBuilder.build()
.get()
.uri(REGISTRY_URL + "/v2/" + repository + "/manifests/" + tag)
.header("Accept", "application/vnd.oci.image.index.v1+json")
.retrieve()
.toBodilessEntity()
.map(resp -> resp.getHeaders().getFirst("Docker-Content-Digest"));
}
}
Loading…
Cancel
Save