[UPDATE] Experiments API에 Kubeflow 연동 로직 추가 및 엔티티 필드 리팩토링 (ExperimentsController, ExperimentsEntity)

main
bjkim 9 months ago
parent a9aa1df85f
commit 521c5735ae

@ -7,21 +7,39 @@ import kr.re.etri.autoflow.entity.ExperimentsEntity;
import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest;
import kr.re.etri.autoflow.service.ExperimentsService; import kr.re.etri.autoflow.service.ExperimentsService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject; import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
@Tag(name = "Experiments", description = "Kubeflow 및 MLflow Experiment API") @Tag(name = "Experiments", description = "Kubeflow 및 MLflow Experiment API")
@RestController @RestController
@RequestMapping("/api/experiments") @RequestMapping("/api/experiments")
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class ExperimentsController { public class ExperimentsController {
private final ExperimentsService experimentsService; private final ExperimentsService experimentsService;
private final WebClient.Builder webClientBuilder;
@Value("${kubeflow.url}")
private String kubeflowBaseUrl; // 예: http://192.168.10.135:32473/
@Operation(summary = "모든 Experiments 조회") @Operation(summary = "모든 Experiments 조회")
@GetMapping @GetMapping
public ResponseEntity<List<ExperimentsEntity>> getAllExperiments() { public ResponseEntity<List<ExperimentsEntity>> getAllExperiments() {
@ -48,9 +66,42 @@ public class ExperimentsController {
@Operation(summary = "Experiment 등록") @Operation(summary = "Experiment 등록")
@PostMapping @PostMapping
public ResponseEntity<ExperimentsEntity> createExperiment(@RequestBody ExperimentsEntity experiment) { public Mono<ResponseEntity<ExperimentsEntity>> createExperiment(@RequestBody ExperimentsEntity experiment) {
return ResponseEntity.ok(experimentsService.save(experiment));
// 1⃣ DB 저장
ExperimentsEntity saved = experimentsService.save(experiment);
// 2⃣ Kubeflow POST 요청 payload
Map<String, Object> payload = new HashMap<>();
payload.put("display_name", saved.getDisplayName());
payload.put("description", saved.getDescription());
payload.put("namespace", "default"); // 필요에 따라 변경
// 3⃣ WebClient POST
return webClientBuilder.build()
.post()
.uri(kubeflowBaseUrl + "/apis/v2beta1/experiments")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(payload)
.retrieve()
.bodyToMono(Map.class) // Kubeflow 응답
.map(resp -> {
// resp에서 필요한 값 추출 후 entity에 반영
if (resp.get("last_run_created_at") != null) {
String lastRunStr = resp.get("last_run_created_at").toString();
OffsetDateTime odt = OffsetDateTime.parse(lastRunStr);
saved.setLastRunCreatedAt(odt.withOffsetSameInstant(ZoneId.of("Asia/Seoul").getRules().getOffset(odt.toInstant()))
.toLocalDateTime());
}
if(resp.get("id") != null) {
saved.setKubeFlowId(resp.get("id").toString());
} }
return saved;
})
.map(entity -> ResponseEntity.ok(entity))
.doOnError(e -> log.error("Kubeflow experiment 등록 실패", e));
}
@Operation(summary = "Experiment 수정") @Operation(summary = "Experiment 수정")
@PutMapping("/{id}") @PutMapping("/{id}")

@ -112,32 +112,32 @@ public class MinioAttachmentController {
} }
} }
@GetMapping("/download_new") // @GetMapping("/download_new")
public ResponseEntity<Resource> downloadFile_new(@RequestParam String objectName) { // public ResponseEntity<Resource> downloadFile_new(@RequestParam String objectName) {
try { // try {
// MinIO에서 스트리밍으로 가져오기 // // MinIO에서 스트리밍으로 가져오기
InputStream is = minioClient.getObject( // InputStream is = minioClient.getObject(
GetObjectArgs.builder() // GetObjectArgs.builder()
.bucket("mlpipeline") // .bucket("mlpipeline")
.object(objectName) // .object(objectName)
.build() // .build()
); // );
//
InputStreamResource resource = new InputStreamResource(is); // InputStreamResource resource = new InputStreamResource(is);
//
String encodedFileName = URLEncoder.encode(objectName, StandardCharsets.UTF_8) // String encodedFileName = URLEncoder.encode(objectName, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20"); // .replaceAll("\\+", "%20");
//
return ResponseEntity.ok() // return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFileName) // .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFileName)
.contentType(MediaType.APPLICATION_OCTET_STREAM) // .contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource); // .body(resource);
//
} catch (Exception e) { // } catch (Exception e) {
log.error("파일 다운로드 실패", e); // log.error("파일 다운로드 실패", e);
return ResponseEntity.internalServerError().build(); // return ResponseEntity.internalServerError().build();
} // }
} // }
@Operation(summary = "MinIO YAML 파일 읽기", description = "MinIO에서 YAML 파일을 다운로드하여 텍스트로 반환합니다.") @Operation(summary = "MinIO YAML 파일 읽기", description = "MinIO에서 YAML 파일을 다운로드하여 텍스트로 반환합니다.")

@ -22,7 +22,7 @@ public class ExperimentsEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "DB PK") @Schema(description = "DB PK", example = "null")
private Long id; private Long id;
@Schema(description = "Kubeflow / MLflow Experiment ID") @Schema(description = "Kubeflow / MLflow Experiment ID")
@ -41,13 +41,13 @@ public class ExperimentsEntity {
private String description; private String description;
@Schema(description = "MLflow Artifact 경로 / Kubeflow에는 해당 없음") @Schema(description = "MLflow Artifact 경로 / Kubeflow에는 해당 없음")
private String artifactLocation; private String mlflow_artifactLocation;
@Schema(description = "MLflow 상태 / lifecycle_stage, Kubeflow에는 해당 없음") @Schema(description = "MLflow 상태 / lifecycle_stage, Kubeflow에는 해당 없음")
private String lifecycleStage; private String mlflowLifecycleStage;
@Schema(description = "Kubeflow 저장 상태 / storage_state, MLflow에는 해당 없음") @Schema(description = "Kubeflow 저장 상태 / storage_state, MLflow에는 해당 없음")
private String storageState; private String kubeflowStorageState;
@CreatedDate @CreatedDate
@Schema(description = "Experiment 생성 일시 / Kubeflow: ISO 8601 created_at, MLflow: timestamp(ms) → LocalDateTime") @Schema(description = "Experiment 생성 일시 / Kubeflow: ISO 8601 created_at, MLflow: timestamp(ms) → LocalDateTime")
@ -63,7 +63,7 @@ public class ExperimentsEntity {
private LocalDateTime lastUpdateTime; private LocalDateTime lastUpdateTime;
@Schema(description = "Kubeflow 마지막 Run 생성 일시 / last_run_created_at") @Schema(description = "Kubeflow 마지막 Run 생성 일시 / last_run_created_at")
private LocalDateTime lastRunCreatedAt; private LocalDateTime kubeflowLastRunCreatedAt;
@Schema(description = "등록자 ID") @Schema(description = "등록자 ID")
private String regUserId; private String regUserId;

Loading…
Cancel
Save