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…
Reference in new issue