|
|
|
|
@ -4,27 +4,21 @@ import kr.re.etri.autoflow.payload.request.CreateRunRequest;
|
|
|
|
|
import kr.re.etri.autoflow.payload.request.RunCreatedEvent;
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
import org.springframework.batch.core.Job;
|
|
|
|
|
import org.springframework.batch.core.JobParametersBuilder;
|
|
|
|
|
import org.springframework.batch.core.launch.JobLauncher;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
import org.springframework.context.ApplicationEventPublisher;
|
|
|
|
|
import org.springframework.http.HttpEntity;
|
|
|
|
|
import org.springframework.http.HttpHeaders;
|
|
|
|
|
import org.springframework.http.MediaType;
|
|
|
|
|
import org.springframework.http.ResponseEntity;
|
|
|
|
|
import org.springframework.core.io.InputStreamResource;
|
|
|
|
|
import org.springframework.http.*;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
import org.springframework.util.LinkedMultiValueMap;
|
|
|
|
|
import org.springframework.util.MultiValueMap;
|
|
|
|
|
import org.springframework.web.client.RestTemplate;
|
|
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
import org.springframework.web.reactive.function.BodyInserters;
|
|
|
|
|
import org.springframework.web.reactive.function.client.WebClient;
|
|
|
|
|
import org.springframework.web.util.UriComponentsBuilder;
|
|
|
|
|
import reactor.core.publisher.Mono;
|
|
|
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.net.URI;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.concurrent.CompletableFuture;
|
|
|
|
|
|
|
|
|
|
@ -36,117 +30,289 @@ public class PipelineUploadService {
|
|
|
|
|
private final RestTemplate restTemplate;
|
|
|
|
|
|
|
|
|
|
@Value("${kubeflow.url}")
|
|
|
|
|
private String kubeflowBaseUrl; // 예: http://192.168.10.135:32473/
|
|
|
|
|
private String kubeflowBaseUrl;
|
|
|
|
|
|
|
|
|
|
private final WebClient webClient;
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
private ApplicationEventPublisher eventPublisher;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Pipeline 업로드
|
|
|
|
|
*/
|
|
|
|
|
public Map uploadPipeline(MultipartFile file,
|
|
|
|
|
String name,
|
|
|
|
|
String displayName,
|
|
|
|
|
String description,
|
|
|
|
|
String namespace) {
|
|
|
|
|
public Map uploadPipeline(
|
|
|
|
|
MultipartFile file,
|
|
|
|
|
String name,
|
|
|
|
|
String displayName,
|
|
|
|
|
String description,
|
|
|
|
|
String namespace
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
|
|
|
|
body.add("uploadfile", new MultipartInputStreamFileResource(file.getInputStream(), file.getOriginalFilename()));
|
|
|
|
|
|
|
|
|
|
log.info("""
|
|
|
|
|
===== Pipeline Upload Start =====
|
|
|
|
|
filename={}
|
|
|
|
|
name={}
|
|
|
|
|
displayName={}
|
|
|
|
|
description={}
|
|
|
|
|
namespace={}
|
|
|
|
|
kubeflowBaseUrl={}
|
|
|
|
|
""",
|
|
|
|
|
file.getOriginalFilename(),
|
|
|
|
|
name,
|
|
|
|
|
displayName,
|
|
|
|
|
description,
|
|
|
|
|
namespace,
|
|
|
|
|
kubeflowBaseUrl
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
MultiValueMap<String, Object> body =
|
|
|
|
|
new LinkedMultiValueMap<>();
|
|
|
|
|
|
|
|
|
|
body.add(
|
|
|
|
|
"uploadfile",
|
|
|
|
|
new MultipartInputStreamFileResource(
|
|
|
|
|
file.getInputStream(),
|
|
|
|
|
file.getOriginalFilename()
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
|
|
|
|
|
|
|
|
|
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
|
|
|
|
|
HttpEntity<MultiValueMap<String, Object>> requestEntity =
|
|
|
|
|
new HttpEntity<>(body, headers);
|
|
|
|
|
|
|
|
|
|
String normalizedBaseUrl = normalizeBaseUrl(kubeflowBaseUrl);
|
|
|
|
|
|
|
|
|
|
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(kubeflowBaseUrl + "apis/v2beta1/pipelines/upload");
|
|
|
|
|
URI uri = UriComponentsBuilder
|
|
|
|
|
.fromHttpUrl(normalizedBaseUrl)
|
|
|
|
|
.path("/apis/v2beta1/pipelines/upload")
|
|
|
|
|
.queryParamIfPresent("name",
|
|
|
|
|
optional(name))
|
|
|
|
|
.queryParamIfPresent("display_name",
|
|
|
|
|
optional(displayName))
|
|
|
|
|
.queryParamIfPresent("description",
|
|
|
|
|
optional(description))
|
|
|
|
|
.queryParamIfPresent("namespace",
|
|
|
|
|
optional(namespace))
|
|
|
|
|
.build(true)
|
|
|
|
|
.toUri();
|
|
|
|
|
|
|
|
|
|
if (name != null && !name.isBlank()) builder.queryParam("name", name);
|
|
|
|
|
if (displayName != null && !displayName.isBlank()) builder.queryParam("display_name", displayName);
|
|
|
|
|
if (description != null && !description.isBlank()) builder.queryParam("description", description);
|
|
|
|
|
if (namespace != null && !namespace.isBlank()) builder.queryParam("namespace", namespace);
|
|
|
|
|
log.info("""
|
|
|
|
|
===== Final Upload URI =====
|
|
|
|
|
uri={}
|
|
|
|
|
""",
|
|
|
|
|
uri
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
ResponseEntity<Map> response =
|
|
|
|
|
restTemplate.postForEntity(
|
|
|
|
|
uri,
|
|
|
|
|
requestEntity,
|
|
|
|
|
Map.class
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
log.info("""
|
|
|
|
|
===== Pipeline Upload Success =====
|
|
|
|
|
status={}
|
|
|
|
|
body={}
|
|
|
|
|
""",
|
|
|
|
|
response.getStatusCode(),
|
|
|
|
|
response.getBody()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
ResponseEntity<Map> response = restTemplate.postForEntity(builder.toUriString(), requestEntity, Map.class);
|
|
|
|
|
return response.getBody();
|
|
|
|
|
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
throw new RuntimeException("Pipeline upload failed", e);
|
|
|
|
|
|
|
|
|
|
log.error("""
|
|
|
|
|
===== Pipeline Upload Failed =====
|
|
|
|
|
filename={}
|
|
|
|
|
kubeflowBaseUrl={}
|
|
|
|
|
""",
|
|
|
|
|
file.getOriginalFilename(),
|
|
|
|
|
kubeflowBaseUrl,
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
throw new RuntimeException(
|
|
|
|
|
"Pipeline upload failed",
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Run 생성
|
|
|
|
|
* runRequest에는 display_name, pipeline_version_reference, runtime_config 등이 포함되어야 함
|
|
|
|
|
*/
|
|
|
|
|
public Map<String, Object> createRun(CreateRunRequest runRequest) {
|
|
|
|
|
// Run 생성 결과를 동기적으로 받아 반환
|
|
|
|
|
public Map<String, Object> createRun(
|
|
|
|
|
CreateRunRequest runRequest
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
String normalizedBaseUrl =
|
|
|
|
|
normalizeBaseUrl(kubeflowBaseUrl);
|
|
|
|
|
|
|
|
|
|
String uri = normalizedBaseUrl +
|
|
|
|
|
"/apis/v2beta1/runs";
|
|
|
|
|
|
|
|
|
|
log.info("Create Run URI={}", uri);
|
|
|
|
|
|
|
|
|
|
Map result = webClient.post()
|
|
|
|
|
.uri(kubeflowBaseUrl + "/apis/v2beta1/runs")
|
|
|
|
|
.uri(uri)
|
|
|
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
.bodyValue(runRequest)
|
|
|
|
|
.retrieve()
|
|
|
|
|
.bodyToMono(Map.class)
|
|
|
|
|
.block(); // 반환은 Map이므로 여기서는 block 유지
|
|
|
|
|
.block();
|
|
|
|
|
|
|
|
|
|
// 이벤트 발행만 비동기로 처리
|
|
|
|
|
if (result != null && result.get("run_id") != null) {
|
|
|
|
|
String runId = (String) result.get("run_id");
|
|
|
|
|
// 이벤트 발행을 비동기 스레드에서 실행
|
|
|
|
|
CompletableFuture.runAsync(() -> eventPublisher.publishEvent(new RunCreatedEvent(runId)));
|
|
|
|
|
if (result != null &&
|
|
|
|
|
result.get("run_id") != null) {
|
|
|
|
|
|
|
|
|
|
String runId =
|
|
|
|
|
(String) result.get("run_id");
|
|
|
|
|
|
|
|
|
|
CompletableFuture.runAsync(() ->
|
|
|
|
|
eventPublisher.publishEvent(
|
|
|
|
|
new RunCreatedEvent(runId)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Experiments 조회
|
|
|
|
|
*/
|
|
|
|
|
public Map listExperiments(String namespace, int pageSize, String pageToken) {
|
|
|
|
|
public Map listExperiments(
|
|
|
|
|
String namespace,
|
|
|
|
|
int pageSize,
|
|
|
|
|
String pageToken
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
UriComponentsBuilder builder = UriComponentsBuilder
|
|
|
|
|
.fromHttpUrl(kubeflowBaseUrl + "/apis/v2beta1/experiments");
|
|
|
|
|
|
|
|
|
|
if (namespace != null && !namespace.isBlank()) {
|
|
|
|
|
builder.queryParam("namespace", namespace);
|
|
|
|
|
}
|
|
|
|
|
if (pageSize > 0) {
|
|
|
|
|
builder.queryParam("page_size", pageSize);
|
|
|
|
|
}
|
|
|
|
|
if (pageToken != null && !pageToken.isBlank()) {
|
|
|
|
|
builder.queryParam("page_token", pageToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String normalizedBaseUrl =
|
|
|
|
|
normalizeBaseUrl(kubeflowBaseUrl);
|
|
|
|
|
|
|
|
|
|
URI uri = UriComponentsBuilder
|
|
|
|
|
.fromHttpUrl(normalizedBaseUrl)
|
|
|
|
|
.path("/apis/v2beta1/experiments")
|
|
|
|
|
.queryParamIfPresent(
|
|
|
|
|
"namespace",
|
|
|
|
|
optional(namespace)
|
|
|
|
|
)
|
|
|
|
|
.queryParamIfPresent(
|
|
|
|
|
"page_token",
|
|
|
|
|
optional(pageToken)
|
|
|
|
|
)
|
|
|
|
|
.queryParam(
|
|
|
|
|
"page_size",
|
|
|
|
|
pageSize
|
|
|
|
|
)
|
|
|
|
|
.build(true)
|
|
|
|
|
.toUri();
|
|
|
|
|
|
|
|
|
|
log.info("List Experiments URI={}", uri);
|
|
|
|
|
|
|
|
|
|
return webClient.get()
|
|
|
|
|
.uri(builder.toUriString())
|
|
|
|
|
.uri(uri)
|
|
|
|
|
.accept(MediaType.APPLICATION_JSON)
|
|
|
|
|
.retrieve()
|
|
|
|
|
.bodyToMono(Map.class)
|
|
|
|
|
.block();
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
throw new RuntimeException("Kubeflow Experiments 조회 실패", e);
|
|
|
|
|
|
|
|
|
|
log.error("Experiment list failed", e);
|
|
|
|
|
|
|
|
|
|
throw new RuntimeException(
|
|
|
|
|
"Kubeflow Experiments 조회 실패",
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Experiment 단건 조회
|
|
|
|
|
*/
|
|
|
|
|
public Map getExperimentById(String experimentId) {
|
|
|
|
|
public Map getExperimentById(
|
|
|
|
|
String experimentId
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
String url = kubeflowBaseUrl + "/apis/v2beta1/experiments/" + experimentId;
|
|
|
|
|
|
|
|
|
|
String normalizedBaseUrl =
|
|
|
|
|
normalizeBaseUrl(kubeflowBaseUrl);
|
|
|
|
|
|
|
|
|
|
URI uri = UriComponentsBuilder
|
|
|
|
|
.fromHttpUrl(normalizedBaseUrl)
|
|
|
|
|
.path("/apis/v2beta1/experiments/{id}")
|
|
|
|
|
.buildAndExpand(experimentId)
|
|
|
|
|
.toUri();
|
|
|
|
|
|
|
|
|
|
log.info("Get Experiment URI={}", uri);
|
|
|
|
|
|
|
|
|
|
return webClient.get()
|
|
|
|
|
.uri(url)
|
|
|
|
|
.uri(uri)
|
|
|
|
|
.accept(MediaType.APPLICATION_JSON)
|
|
|
|
|
.retrieve()
|
|
|
|
|
.bodyToMono(Map.class)
|
|
|
|
|
.block();
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
throw new RuntimeException("Kubeflow experiment 조회 실패: " + experimentId, e);
|
|
|
|
|
|
|
|
|
|
log.error(
|
|
|
|
|
"Experiment 조회 실패. experimentId={}",
|
|
|
|
|
experimentId,
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
throw new RuntimeException(
|
|
|
|
|
"Kubeflow experiment 조회 실패: "
|
|
|
|
|
+ experimentId,
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String normalizeBaseUrl(
|
|
|
|
|
String baseUrl
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
if (baseUrl == null ||
|
|
|
|
|
baseUrl.isBlank()) {
|
|
|
|
|
|
|
|
|
|
throw new IllegalArgumentException(
|
|
|
|
|
"kubeflow.url is empty"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
baseUrl = baseUrl.trim();
|
|
|
|
|
|
|
|
|
|
if (baseUrl.endsWith("/")) {
|
|
|
|
|
baseUrl =
|
|
|
|
|
baseUrl.substring(
|
|
|
|
|
0,
|
|
|
|
|
baseUrl.length() - 1
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return baseUrl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private java.util.Optional<String> optional(
|
|
|
|
|
String value
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
if (value == null ||
|
|
|
|
|
value.isBlank()) {
|
|
|
|
|
|
|
|
|
|
return java.util.Optional.empty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return java.util.Optional.of(value);
|
|
|
|
|
}
|
|
|
|
|
}
|