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