diff --git a/src/main/java/kr/re/etri/autoflow/controllers/PipelineUploadController.java b/src/main/java/kr/re/etri/autoflow/controllers/PipelineUploadController.java index 5814cc0..c13e751 100644 --- a/src/main/java/kr/re/etri/autoflow/controllers/PipelineUploadController.java +++ b/src/main/java/kr/re/etri/autoflow/controllers/PipelineUploadController.java @@ -1,7 +1,11 @@ package kr.re.etri.autoflow.controllers; -import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import kr.re.etri.autoflow.entity.WorkflowEntity; +import kr.re.etri.autoflow.payload.request.CreateRunRequest; import kr.re.etri.autoflow.service.PipelineUploadService; import kr.re.etri.autoflow.service.WorkFlowService; import lombok.RequiredArgsConstructor; @@ -10,58 +14,40 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.multipart.MultipartFile; -import java.time.LocalDateTime; -import java.util.HashMap; import java.util.Map; @Slf4j @RestController @RequestMapping("/pipelines") @RequiredArgsConstructor -@CrossOrigin(origins = "*") // 모든 도메인 허용 -@io.swagger.v3.oas.annotations.tags.Tag(name = "Kubeflow Pipeline", description = "Kubeflow 파이프라인 업로드 API") +@CrossOrigin(origins = "*") +@io.swagger.v3.oas.annotations.tags.Tag(name = "Kubeflow Pipeline", description = "Kubeflow 파이프라인 API") public class PipelineUploadController { private final PipelineUploadService pipelineUploadService; private final WorkFlowService workFlowService; - @io.swagger.v3.oas.annotations.Operation( - summary = "파이프라인 업로드", - description = "Kubeflow에 파이프라인 파일(Multipart)을 업로드하고, 업로드 결과를 반환합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "파이프라인 업로드 성공" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "400", - description = "잘못된 요청" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "500", - description = "서버 내부 오류" - ) + @Operation(summary = "파이프라인 업로드", description = "Kubeflow에 파이프라인 파일(Multipart)을 업로드") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "파이프라인 업로드 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청"), + @ApiResponse(responseCode = "500", description = "서버 내부 오류") }) - @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> uploadPipeline( @RequestParam("uploadfile") MultipartFile file, - @RequestParam(value = "name", required = true) String name, - @RequestParam(value = "display_name", required = true) String displayName, + @RequestParam("name") String name, + @RequestParam("display_name") String displayName, @RequestParam(value = "description", required = false) String description, @RequestParam(value = "namespace", required = false) String namespace, - @RequestParam(value = "regUserId") String regUserId, - @RequestParam(value = "projectId") Long projectId + @RequestParam("regUserId") String regUserId, + @RequestParam("projectId") Long projectId ) { try { - // Kubeflow 업로드 Map result = pipelineUploadService.uploadPipeline(file, name, displayName, description, namespace); - // WorkflowEntity 매핑 후 DB 저장 WorkflowEntity workflow = WorkflowEntity.builder() .pipelineId((String) result.get("pipeline_id")) .displayName((String) result.get("display_name")) @@ -74,19 +60,50 @@ public class PipelineUploadController { .version(1) .build(); - // 저장 workFlowService.save(workflow); return ResponseEntity.ok(result); } catch (Exception e) { log.error("Pipeline upload failed", e); - Map error = Map.of( - "status", 500, - "error", "Internal Server Error", - "message", e.getMessage() - ); - return ResponseEntity.status(500).body(error); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of( + "status", 500, + "error", "Internal Server Error", + "message", e.getMessage() + )); + } + } + + @PostMapping("/runs") + @Operation(summary = "Kubeflow Run 생성", description = "Kubeflow에 Run을 생성합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Run 생성 성공"), + @ApiResponse(responseCode = "400", description = "display_name 필수"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + public ResponseEntity> createRun( + @Parameter @RequestBody CreateRunRequest runRequest + ) { + try { + Map result = pipelineUploadService.createRun(runRequest); + return ResponseEntity.ok(result); + } catch (IllegalArgumentException e) { + log.error("Invalid run request", e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of( + "status", 400, + "error", "Bad Request", + "message", e.getMessage() + )); + } catch (Exception e) { + log.error("Run creation failed", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of( + "status", 500, + "error", "Internal Server Error", + "message", e.getMessage() + )); } } } diff --git a/src/main/java/kr/re/etri/autoflow/payload/request/CreateRunRequest.java b/src/main/java/kr/re/etri/autoflow/payload/request/CreateRunRequest.java new file mode 100644 index 0000000..ecb239c --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/payload/request/CreateRunRequest.java @@ -0,0 +1,43 @@ +package kr.re.etri.autoflow.payload.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Map; + +@Data +@Schema(description = "Kubeflow Run 생성 요청 DTO") +public class CreateRunRequest { + + @Schema(description = "Run 이름 (필수)", required = true, example = "Run of 435345 (5c7b9)") + private String display_name; + + @Schema(description = "Run 설명", example = "테스트 Run") + private String description; + + @Schema(description = "파이프라인 버전 참조", required = true) + private PipelineVersionReference pipeline_version_reference; + + @Schema(description = "런 타임 구성") + private RuntimeConfig runtime_config; + + @Schema(description = "서비스 계정", example = "pipeline-runner") + private String service_account; + + @Data + @Schema(description = "Pipeline Version Reference") + public static class PipelineVersionReference { + @Schema(description = "파이프라인 ID", required = true) + private String pipeline_id; + + @Schema(description = "파이프라인 버전 ID", required = true) + private String pipeline_version_id; + } + + @Data + @Schema(description = "Runtime Config") + public static class RuntimeConfig { + @Schema(description = "파라미터", example = "{}") + private Map parameters; + } +} diff --git a/src/main/java/kr/re/etri/autoflow/service/PipelineUploadService.java b/src/main/java/kr/re/etri/autoflow/service/PipelineUploadService.java index bd26324..7bf154a 100644 --- a/src/main/java/kr/re/etri/autoflow/service/PipelineUploadService.java +++ b/src/main/java/kr/re/etri/autoflow/service/PipelineUploadService.java @@ -1,6 +1,9 @@ package kr.re.etri.autoflow.service; +import com.fasterxml.jackson.databind.ObjectMapper; +import kr.re.etri.autoflow.payload.request.CreateRunRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -18,20 +21,23 @@ import java.util.Map; @Service @RequiredArgsConstructor +@Slf4j public class PipelineUploadService { private final RestTemplate restTemplate; - @Value("${kubeflow.pipeline.upload-url}") - private String kubeflowUploadUrl; + @Value("${kubeflow.url}") + private String kubeflowBaseUrl; // 예: http://192.168.10.135:32473/ - public Map uploadPipeline(MultipartFile file, - String name, - String displayName, - String description, - String namespace) { + /** + * Pipeline 업로드 + */ + public Map uploadPipeline(MultipartFile file, + String name, + String displayName, + String description, + String namespace) { try { - // 파일 form-data MultiValueMap body = new LinkedMultiValueMap<>(); body.add("uploadfile", new MultipartInputStreamFileResource(file.getInputStream(), file.getOriginalFilename())); @@ -40,21 +46,47 @@ public class PipelineUploadService { HttpEntity> requestEntity = new HttpEntity<>(body, headers); - // URL 조립 (쿼리 파라미터 방식) - UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(kubeflowUploadUrl); + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(kubeflowBaseUrl + "apis/v2beta1/pipelines/upload"); 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); - String url = builder.toUriString(); - - ResponseEntity response = restTemplate.postForEntity(url, requestEntity, Map.class); + ResponseEntity response = restTemplate.postForEntity(builder.toUriString(), requestEntity, Map.class); return response.getBody(); } catch (IOException e) { throw new RuntimeException("Pipeline upload failed", e); } } + + /** + * Run 생성 + * runRequest에는 display_name, pipeline_version_reference, runtime_config 등이 포함되어야 함 + */ +// PipelineUploadService.java + public Map createRun(CreateRunRequest runRequest) { + try { + // URL 조립 + String url = kubeflowBaseUrl + "apis/v2beta1/runs"; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + // DTO를 Map으로 변환해서 전송 + ObjectMapper objectMapper = new ObjectMapper(); + Map body = objectMapper.convertValue(runRequest, Map.class); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + ResponseEntity response = restTemplate.postForEntity(url, requestEntity, Map.class); + + return response.getBody(); + } catch (Exception e) { + if (e instanceof IllegalArgumentException) throw e; + throw new RuntimeException("Kubeflow Run creation failed", e); + } + } + }