@ -4,16 +4,22 @@ import kr.re.etri.autoflow.payload.request.CreateRunRequest;
import kr.re.etri.autoflow.payload.request.RunCreatedEvent ;
import kr.re.etri.autoflow.payload.request.RunCreatedEvent ;
import lombok.RequiredArgsConstructor ;
import lombok.RequiredArgsConstructor ;
import lombok.extern.slf4j.Slf4j ;
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.Autowired ;
import org.springframework.beans.factory.annotation.Value ;
import org.springframework.beans.factory.annotation.Value ;
import org.springframework.context.ApplicationEventPublisher ;
import org.springframework.context.ApplicationEventPublisher ;
import org.springframework.core.io.InputStreamResource ;
import org.springframework.http.HttpEntity ;
import org.springframework.http.* ;
import org.springframework.http.HttpHeaders ;
import org.springframework.http.MediaType ;
import org.springframework.http.ResponseEntity ;
import org.springframework.stereotype.Service ;
import org.springframework.stereotype.Service ;
import org.springframework.util.LinkedMultiValueMap ;
import org.springframework.util.LinkedMultiValueMap ;
import org.springframework.util.MultiValueMap ;
import org.springframework.util.MultiValueMap ;
import org.springframework.web.client.RestTemplate ;
import org.springframework.web.client.RestTemplate ;
import org.springframework.web.multipart.MultipartFile ;
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.reactive.function.client.WebClient ;
import org.springframework.web.reactive.function.client.WebClientResponseException ;
import org.springframework.web.reactive.function.client.WebClientResponseException ;
import org.springframework.web.util.UriComponentsBuilder ;
import org.springframework.web.util.UriComponentsBuilder ;
@ -21,10 +27,13 @@ import org.springframework.web.util.UriUtils;
import reactor.core.publisher.Mono ;
import reactor.core.publisher.Mono ;
import java.io.IOException ;
import java.io.IOException ;
import java.net.URI ;
import java.nio.charset.StandardCharsets ;
import java.nio.charset.StandardCharsets ;
import java.time.Duration ;
import java.time.Duration ;
import java.util.* ;
import java.util.Collections ;
import java.util.HashMap ;
import java.util.HashSet ;
import java.util.Map ;
import java.util.Set ;
import java.util.concurrent.CompletableFuture ;
import java.util.concurrent.CompletableFuture ;
@Service
@Service
@ -34,629 +43,323 @@ public class PipelineUploadService {
private final RestTemplate restTemplate ;
private final RestTemplate restTemplate ;
private final WebClient webClient ;
@Value ( "${kubeflow.url}" )
@Value ( "${kubeflow.url}" )
private String kubeflowBaseUrl ;
private String kubeflowBaseUrl ; // 예: http://192.168.10.135:32473/
private final WebClient webClient ;
@Autowired
@Autowired
private ApplicationEventPublisher eventPublisher ;
private ApplicationEventPublisher eventPublisher ;
/ * *
/ * *
* Pipeline 업 로 드
* Pipeline 업 로 드
* /
* /
public Map uploadPipeline (
public Map uploadPipeline ( MultipartFile file ,
MultipartFile file ,
String name ,
String name ,
String displayName ,
String displayName ,
String description ,
String description ,
String namespace
String namespace ) {
) {
try {
try {
MultiValueMap < String , Object > body = new LinkedMultiValueMap < > ( ) ;
log . info ( "" "
body . add ( "uploadfile" , new MultipartInputStreamFileResource ( file . getInputStream ( ) , file . getOriginalFilename ( ) ) ) ;
= = = = = Pipeline Upload Start = = = = =
filename = { }
name = { }
displayName = { }
description = { }
namespace = { }
"" " ,
file . getOriginalFilename ( ) ,
name ,
displayName ,
description ,
namespace
) ;
MultiValueMap < String , Object > body =
new LinkedMultiValueMap < > ( ) ;
body . add (
"uploadfile" ,
new MultipartInputStreamFileResource (
file . getInputStream ( ) ,
file . getOriginalFilename ( )
)
) ;
HttpHeaders headers = new HttpHeaders ( ) ;
HttpHeaders headers = new HttpHeaders ( ) ;
headers . setContentType (
headers . setContentType ( MediaType . MULTIPART_FORM_DATA ) ;
MediaType . MULTIPART_FORM_DATA
) ;
HttpEntity < MultiValueMap < String , Object > >
requestEntity =
new HttpEntity < > ( body , headers ) ;
URI uri = UriComponentsBuilder
HttpEntity < MultiValueMap < String , Object > > requestEntity = new HttpEntity < > ( body , headers ) ;
. fromHttpUrl (
normalizeBaseUrl (
kubeflowBaseUrl
)
)
. 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 ( ) ;
log. info ( "Pipeline Upload URI={}" , uri ) ;
UriComponentsBuilder builder = UriComponentsBuilder . fromUriString ( kubeflowBaseUrl . replaceAll ( "/+$" , "" ) + "/apis/v2beta1/pipelines/upload" ) ;
ResponseEntity < Map > response =
if ( name ! = null & & ! name . isBlank ( ) ) builder . queryParam ( "name" , name ) ;
restTemplate . postForEntity (
if ( displayName ! = null & & ! displayName . isBlank ( ) ) builder . queryParam ( "display_name" , displayName ) ;
uri ,
if ( description ! = null & & ! description . isBlank ( ) ) builder . queryParam ( "description" , description ) ;
requestEntity ,
if ( namespace ! = null & & ! namespace . isBlank ( ) ) builder . queryParam ( "namespace" , namespace ) ;
Map . class
) ;
ResponseEntity < Map > response = restTemplate . postForEntity ( builder . toUriString ( ) , requestEntity , Map . class ) ;
return response . getBody ( ) ;
return response . getBody ( ) ;
} catch ( IOException e ) {
} catch ( IOException e ) {
throw new RuntimeException ( "Pipeline upload failed" , e ) ;
log . error (
"Pipeline upload failed" ,
e
) ;
throw new RuntimeException (
"Pipeline upload failed" ,
e
) ;
}
}
}
}
/ * *
/ * *
* Run 생 성
* Run 생 성
* runRequest 에 는 display_name , pipeline_version_reference , runtime_config 등 이 포 함 되 어 야 함 .
* KFP v2beta1 는 runtime_config 필 수 , display_name 필 수 .
* parameters 는 파 이 프 라 인 스 펙 에 정 의 된 입 력 만 전 달 하 며 , mlflow_experiment_name 은 파 이 프 라 인 에 정 의 돼 있 을 때 만 포 함 한 다 .
* /
* /
public Map < String , Object > createRun (
public Map < String , Object > createRun ( CreateRunRequest runRequest ) {
CreateRunRequest runRequest
Set < String > allowedParamNames = getPipelineRootParameterNames ( runRequest ) ;
) {
Map < String , Object > body = buildKfpRunRequestBody ( runRequest , allowedParamNames ) ;
log . debug ( "[KFP] CreateRun request body: {}" , body ) ;
Set < String > allowedParamNames =
getPipelineRootParameterNames (
runRequest
) ;
Map < String , Object > body =
buildKfpRunRequestBody (
runRequest ,
allowedParamNames
) ;
String uri =
normalizeBaseUrl ( kubeflowBaseUrl )
+ "/apis/v2beta1/runs" ;
log . info ( "Create Run URI={}" , uri ) ;
Map result = webClient . post ( )
Map result = webClient . post ( )
. uri ( uri )
. uri ( kubeflowBaseUrl . replaceAll ( "/+$" , "" ) + "/apis/v2beta1/runs" )
. contentType (
. contentType ( MediaType . APPLICATION_JSON )
MediaType . APPLICATION_JSON
)
. bodyValue ( body )
. bodyValue ( body )
. retrieve ( )
. retrieve ( )
. onStatus (
. onStatus ( status - > status . is4xxClientError ( ) | | status . is5xxServerError ( ) ,
status - >
resp - > resp . bodyToMono ( String . class )
status . is4xxClientError ( )
. doOnNext ( msg - > log . warn ( "[KFP] CreateRun error {}: {}" , resp . statusCode ( ) , msg ) )
| | status . is5xxServerError ( ) ,
. map ( msg - > new RuntimeException ( "KFP CreateRun failed: " + resp . statusCode ( ) + " " + msg ) ) )
response - >
response . bodyToMono ( String . class )
. map ( msg - >
new RuntimeException (
"KFP CreateRun failed: "
+ response . statusCode ( )
+ " "
+ msg
)
)
)
. bodyToMono ( Map . class )
. bodyToMono ( Map . class )
. block ( ) ;
. block ( ) ;
if ( result ! = null & &
// 이벤트 발행만 비동기로 처리
result . get ( "run_id" ) ! = null ) {
if ( result ! = null & & result . get ( "run_id" ) ! = null ) {
String runId = ( String ) result . get ( "run_id" ) ;
String runId =
CompletableFuture . runAsync ( ( ) - > eventPublisher . publishEvent ( new RunCreatedEvent ( runId ) ) ) ;
( String ) result . get ( "run_id" ) ;
CompletableFuture . runAsync ( ( ) - >
eventPublisher . publishEvent (
new RunCreatedEvent ( runId )
)
) ;
}
}
return result ;
return result ;
}
}
/ * *
/ * *
* Pipeline Version Spec 조 회
* KFP pipeline version 스 펙 을 조 회 하 여 root 입 력 파 라 미 터 이 름 목 록 을 반 환 한 다 .
* pipeline_version_reference 에 pipeline_id , pipeline_version_id 가 있 을 때 만 조 회 하 며 , 실 패 시 null 반 환 .
* /
* /
@SuppressWarnings ( "unchecked" )
@SuppressWarnings ( "unchecked" )
private Set < String > getPipelineRootParameterNames (
private Set < String > getPipelineRootParameterNames ( CreateRunRequest runRequest ) {
CreateRunRequest runRequest
CreateRunRequest . PipelineVersionReference ref = runRequest ! = null ? runRequest . getPipeline_version_reference ( ) : null ;
) {
if ( ref = = null | | ref . getPipeline_id ( ) = = null | | ref . getPipeline_id ( ) . isBlank ( )
| | ref . getPipeline_version_id ( ) = = null | | ref . getPipeline_version_id ( ) . isBlank ( ) ) {
CreateRunRequest . PipelineVersionReference ref =
runRequest ! = null
? runRequest . getPipeline_version_reference ( )
: null ;
if ( ref = = null
| | ref . getPipeline_id ( ) = = null
| | ref . getPipeline_version_id ( ) = = null ) {
return null ;
return null ;
}
}
String url = kubeflowBaseUrl . replaceAll ( "/+$" , "" ) + "/apis/v2beta1/pipelines/" + ref . getPipeline_id ( ) + "/versions/" + ref . getPipeline_version_id ( ) ;
String url =
normalizeBaseUrl ( kubeflowBaseUrl )
+ "/apis/v2beta1/pipelines/"
+ ref . getPipeline_id ( )
+ "/versions/"
+ ref . getPipeline_version_id ( ) ;
try {
try {
Map < String , Object > version = webClient . get ( )
Map < String , Object > version =
webClient . get ( )
. uri ( url )
. uri ( url )
. accept ( MediaType . APPLICATION_JSON )
. retrieve ( )
. retrieve ( )
. bodyToMono ( Map . class )
. bodyToMono ( Map . class )
. block ( ) ;
. block ( ) ;
if ( version = = null ) return null ;
if ( version = = null ) {
Object spec = version . get ( "pipeline_spec" ) ;
return null ;
if ( ! ( spec instanceof Map ) ) return null ;
}
Map < String , Object > specMap = ( Map < String , Object > ) spec ;
Object root = specMap . get ( "root" ) ;
Map < String , Object > spec =
if ( root = = null ) root = specMap . get ( "Root" ) ;
( Map < String , Object > )
if ( ! ( root instanceof Map ) ) return null ;
version . get ( "pipeline_spec" ) ;
Map < String , Object > rootMap = ( Map < String , Object > ) root ;
Object inputDef = rootMap . get ( "inputDefinitions" ) ;
if ( spec = = null ) {
if ( inputDef = = null ) inputDef = rootMap . get ( "input_definitions" ) ;
return null ;
if ( ! ( inputDef instanceof Map ) ) return null ;
Map < String , Object > inputDefMap = ( Map < String , Object > ) inputDef ;
Object params = inputDefMap . get ( "parameters" ) ;
if ( params = = null ) return Collections . emptySet ( ) ;
Set < String > names = new HashSet < > ( ) ;
if ( params instanceof Map ) {
for ( Object key : ( ( Map < ? , ? > ) params ) . keySet ( ) ) {
if ( key ! = null ) names . add ( key . toString ( ) ) ;
}
}
return names ;
Map < String , Object > root =
( Map < String , Object > )
spec . get ( "root" ) ;
if ( root = = null ) {
root =
( Map < String , Object > )
spec . get ( "Root" ) ;
}
}
if ( params instanceof Iterable ) {
if ( root = = null ) {
for ( Object item : ( Iterable < ? > ) params ) {
return null ;
if ( item instanceof Map ) {
Object name = ( ( Map < ? , ? > ) item ) . get ( "name" ) ;
if ( name ! = null ) names . add ( name . toString ( ) ) ;
}
}
Map < String , Object > inputDefinitions =
( Map < String , Object > )
root . get ( "inputDefinitions" ) ;
if ( inputDefinitions = = null ) {
inputDefinitions =
( Map < String , Object > )
root . get ( "input_definitions" ) ;
}
}
return names ;
if ( inputDefinitions = = null ) {
return null ;
}
}
Object parameters =
inputDefinitions . get ( "parameters" ) ;
if ( ! ( parameters instanceof Map < ? , ? > map ) ) {
return Collections . emptySet ( ) ;
return Collections . emptySet ( ) ;
}
Set < String > names = new HashSet < > ( ) ;
for ( Object key : map . keySet ( ) ) {
names . add ( key . toString ( ) ) ;
}
return names ;
} catch ( Exception e ) {
} catch ( Exception e ) {
log . warn ( "[KFP] Pipeline version spec 조회 실패, parameters 필터 없이 전달 (mlflow_experiment_name 제외): {}" , e . getMessage ( ) ) ;
log . warn (
"Pipeline version spec 조회 실패" ,
e
) ;
return null ;
return null ;
}
}
}
}
/ * *
/ * *
* Run Request Body 생 성
* KFP Run 생 성 API 요 청 body 구 성 .
* v2beta1 Run : experiment_id , display_name ( 필 수 ) , runtime_config ( 필 수 ) , pipeline_version_reference 등 .
* parameters 는 파 이 프 라 인 스 펙 에 정 의 된 입 력 만 포 함 한 다 . allowedParamNames 가 null 이 면 스 펙 조 회 실 패 로 , mlflow_experiment_name 은 넣 지 않 고 그 외 파 라 미 터 만 전 달 한 다 .
* /
* /
private Map < String , Object > buildKfpRunRequestBody (
private Map < String , Object > buildKfpRunRequestBody ( CreateRunRequest runRequest , Set < String > allowedParamNames ) {
CreateRunRequest runRequest ,
Map < String , Object > body = new HashMap < > ( ) ;
Set < String > allowedParamNames
if ( runRequest . getExperiment_id ( ) ! = null & & ! runRequest . getExperiment_id ( ) . isBlank ( ) ) {
) {
body . put ( "experiment_id" , runRequest . getExperiment_id ( ) ) ;
Map < String , Object > body =
new HashMap < > ( ) ;
if ( runRequest . getExperiment_id ( ) ! = null & &
! runRequest . getExperiment_id ( ) . isBlank ( ) ) {
body . put (
"experiment_id" ,
runRequest . getExperiment_id ( )
) ;
}
}
// display_name 필수 (KFP v2beta1)
body . put (
body . put ( "display_name" , runRequest . getDisplay_name ( ) ! = null & & ! runRequest . getDisplay_name ( ) . isBlank ( )
"display_name" ,
runRequest . getDisplay_name ( ) ! = null
& & ! runRequest . getDisplay_name ( ) . isBlank ( )
? runRequest . getDisplay_name ( )
? runRequest . getDisplay_name ( )
: "Run"
: "Run" ) ;
) ;
if ( runRequest . getDescription ( ) ! = null & & ! runRequest . getDescription ( ) . isBlank ( ) ) {
body . put ( "description" , runRequest . getDescription ( ) ) ;
if ( runRequest . getDescription ( ) ! = null & &
}
! runRequest . getDescription ( ) . isBlank ( ) ) {
if ( runRequest . getPipeline_version_reference ( ) ! = null ) {
body . put ( "pipeline_version_reference" , runRequest . getPipeline_version_reference ( ) ) ;
body . put (
}
"description" ,
if ( runRequest . getService_account ( ) ! = null & & ! runRequest . getService_account ( ) . isBlank ( ) ) {
runRequest . getDescription ( )
body . put ( "service_account" , runRequest . getService_account ( ) ) ;
) ;
}
}
// runtime_config 필수 (KFP v2beta1). 파이프라인에 정의된 파라미터만 전달. 정의되지 않으면 mlflow_experiment_name은 넣지 않음.
Map < String , Object > runtimeConfig = new HashMap < > ( ) ;
if ( runRequest . getPipeline_version_reference ( )
if ( runRequest . getRuntime_config ( ) ! = null & & runRequest . getRuntime_config ( ) . getParameters ( ) ! = null
! = null ) {
& & ! runRequest . getRuntime_config ( ) . getParameters ( ) . isEmpty ( ) ) {
Map < String , Object > params = runRequest . getRuntime_config ( ) . getParameters ( ) ;
body . put (
Map < String , Object > kfpParams = new HashMap < > ( ) ;
"pipeline_version_reference" ,
for ( Map . Entry < String , Object > e : params . entrySet ( ) ) {
runRequest . getPipeline_version_reference ( )
String key = e . getKey ( ) ;
) ;
if ( key = = null ) continue ;
}
if ( allowedParamNames ! = null ) {
if ( ! allowedParamNames . contains ( key ) ) continue ;
Map < String , Object > runtimeConfig =
} else {
new HashMap < > ( ) ;
// 스펙 조회 실패 시: mlflow_* 파라미터는 파이프라인에 없을 수 있으므로 제외
if ( key . startsWith ( "mlflow_" ) ) continue ;
if ( runRequest . getRuntime_config ( ) ! = null
}
& & runRequest . getRuntime_config ( )
kfpParams . put ( key , e . getValue ( ) ) ;
. getParameters ( ) ! = null ) {
}
if ( ! kfpParams . isEmpty ( ) ) {
Map < String , Object > filtered =
runtimeConfig . put ( "parameters" , kfpParams ) ;
new HashMap < > ( ) ;
}
}
for ( Map . Entry < String , Object > entry :
body . put ( "runtime_config" , runtimeConfig ) ;
runRequest . getRuntime_config ( )
. getParameters ( )
. entrySet ( ) ) {
String key = entry . getKey ( ) ;
if ( allowedParamNames ! = null & &
! allowedParamNames . contains ( key ) ) {
continue ;
}
filtered . put (
key ,
entry . getValue ( )
) ;
}
if ( ! filtered . isEmpty ( ) ) {
runtimeConfig . put (
"parameters" ,
filtered
) ;
}
}
body . put (
"runtime_config" ,
runtimeConfig
) ;
return body ;
return body ;
}
}
/ * *
/ * *
* Experiments 조 회
* Experiments 조 회
* /
* /
public Map listExperiments (
public Map listExperiments ( String namespace , int pageSize , String pageToken ) {
String namespace ,
try {
int pageSize ,
UriComponentsBuilder builder = UriComponentsBuilder
String pageToken
. fromHttpUrl ( kubeflowBaseUrl . replaceAll ( "/+$" , "" ) + "/apis/v2beta1/experiments" ) ;
) {
URI uri = UriComponentsBuilder
if ( namespace ! = null & & ! namespace . isBlank ( ) ) {
. fromHttpUrl (
builder . queryParam ( "namespace" , namespace ) ;
normalizeBaseUrl (
}
kubeflowBaseUrl
if ( pageSize > 0 ) {
)
builder . queryParam ( "page_size" , pageSize ) ;
)
}
. path ( "/apis/v2beta1/experiments" )
if ( pageToken ! = null & & ! pageToken . isBlank ( ) ) {
. queryParamIfPresent (
builder . queryParam ( "page_token" , pageToken ) ;
"namespace" ,
}
optional ( namespace )
)
. queryParamIfPresent (
"page_token" ,
optional ( pageToken )
)
. queryParam (
"page_size" ,
pageSize
)
. build ( true )
. toUri ( ) ;
return webClient . get ( )
return webClient . get ( )
. uri ( uri )
. uri ( builder . toUriString ( ) )
. accept ( MediaType . APPLICATION_JSON )
. retrieve ( )
. retrieve ( )
. bodyToMono ( Map . class )
. bodyToMono ( Map . class )
. block ( ) ;
. block ( ) ;
} catch ( Exception e ) {
throw new RuntimeException ( "Kubeflow Experiments 조회 실패" , e ) ;
}
}
}
/ * *
/ * *
* Experiment 단 건 조 회
* Experiment 단 건 조 회
* /
* /
public Map getExperimentById (
public Map getExperimentById ( String experimentId ) {
String experimentId
try {
) {
String url = kubeflowBaseUrl . replaceAll ( "/+$" , "" ) + "/apis/v2beta1/experiments/" + experimentId ;
String uri =
normalizeBaseUrl ( kubeflowBaseUrl )
+ "/apis/v2beta1/experiments/"
+ experimentId ;
return webClient . get ( )
return webClient . get ( )
. uri ( uri )
. uri ( url )
. accept ( MediaType . APPLICATION_JSON )
. retrieve ( )
. retrieve ( )
. bodyToMono ( Map . class )
. bodyToMono ( Map . class )
. block ( ) ;
. block ( ) ;
} catch ( Exception e ) {
throw new RuntimeException ( "Kubeflow experiment 조회 실패: " + experimentId , e ) ;
}
}
}
/ * *
/ * *
* Run 단 건 조 회
* KFP Run 단 건 조 회 ( v2beta1 GET / apis / v2beta1 / runs / { run_id } ) .
* run_details . task_details [ ] . pod_name 으 로 UI 로 그 와 동 일 한 Pod 지 정 가 능 .
* /
* /
public Map < String , Object > getKfpRunById (
@SuppressWarnings ( "unchecked" )
String runId
public Map < String , Object > getKfpRunById ( String runId ) {
) {
if ( runId = = null | | runId . isBlank ( ) ) {
if ( runId = = null | |
runId . isBlank ( ) ) {
return null ;
return null ;
}
}
String url = kubeflowBaseUrl . replaceAll ( "/+$" , "" ) + "/apis/v2beta1/runs/" + runId . trim ( ) ;
String uri =
normalizeBaseUrl ( kubeflowBaseUrl )
+ "/apis/v2beta1/runs/"
+ runId . trim ( ) ;
try {
try {
return webClient . get ( )
return webClient . get ( )
. uri ( ur i )
. uri ( url )
. accept ( MediaType . APPLICATION_JSON )
. accept ( MediaType . APPLICATION_JSON )
. retrieve ( )
. retrieve ( )
. bodyToMono ( Map . class )
. bodyToMono ( Map . class )
. block ( ) ;
. block ( ) ;
} catch ( WebClientResponseException e ) {
} catch ( WebClientResponseException e ) {
if ( e . getStatusCode ( ) . value ( ) = = 404 ) {
if ( e . getStatusCode ( ) . value ( ) = = 404 ) {
log . debug ( "[KFP] Run not found: {}" , runId ) ;
log . debug (
} else {
"Run not found. runId={}" ,
log . warn ( "[KFP] GetRun {}: {}" , runId , e . getMessage ( ) ) ;
runId
) ;
return null ;
}
}
return null ;
throw e ;
} catch ( Exception e ) {
log . debug ( "[KFP] GetRun failed {}: {}" , runId , e . getMessage ( ) ) ;
return null ;
}
}
}
}
/ * *
/ * *
* Node Log 조 회
* KFP ml - pipeline 이 클 러 스 터 에 서 읽 는 Pod 로 그 ( KFP UI 와 동 일 한 백 엔 드 경 로 ) .
* { @code GET / apis / v1beta1 / runs / { run_id } / nodes / { node_id } / log }
* < p > v2 Run ID 도 일 부 배 포 에 서 동 작 합 니 다 . 404 / 비 어 있 으 면 null . < / p >
* /
* /
public String getV1beta1RunNodeLog (
public String getV1beta1RunNodeLog ( String runId , String nodeId ) {
String runId ,
if ( runId = = null | | runId . isBlank ( ) | | nodeId = = null | | nodeId . isBlank ( ) ) {
String nodeId
) {
if ( runId = = null | |
nodeId = = null ) {
return null ;
return null ;
}
}
String base = kubeflowBaseUrl . replaceAll ( "/+$" , "" ) ;
String uri =
String encRun = UriUtils . encodePathSegment ( runId . trim ( ) , StandardCharsets . UTF_8 ) ;
normalizeBaseUrl ( kubeflowBaseUrl )
String encNode = UriUtils . encodePathSegment ( nodeId . trim ( ) , StandardCharsets . UTF_8 ) ;
+ "/apis/v1beta1/runs/"
String url = base + "/apis/v1beta1/runs/" + encRun + "/nodes/" + encNode + "/log" ;
+ UriUtils . encodePathSegment (
runId ,
StandardCharsets . UTF_8
)
+ "/nodes/"
+ UriUtils . encodePathSegment (
nodeId ,
StandardCharsets . UTF_8
)
+ "/log" ;
try {
try {
return webClient . get ( )
return webClient . get ( )
. uri ( ur i )
. uri ( url )
. accept ( MediaType . TEXT_PLAIN )
. accept ( MediaType . TEXT_PLAIN , MediaType . APPLICATION_JSON )
. retrieve ( )
. retrieve ( )
. bodyToMono ( String . class )
. bodyToMono ( String . class )
. timeout ( Duration . ofSeconds ( 120 ) )
. timeout ( Duration . ofSeconds ( 120 ) )
. block ( ) ;
. block ( ) ;
} catch ( WebClientResponseException e ) {
int code = e . getStatusCode ( ) . value ( ) ;
if ( code = = 404 | | code = = 400 ) {
log . debug ( "[KFP] v1beta1 node log {} node={}: {}" , runId , nodeId , code ) ;
} else {
log . debug ( "[KFP] v1beta1 node log {} node={}: {}" , runId , nodeId , e . getMessage ( ) ) ;
}
return null ;
} catch ( Exception e ) {
} catch ( Exception e ) {
log . debug ( "[KFP] v1beta1 node log failed runId={} node={}: {}" , runId , nodeId , e . getMessage ( ) ) ;
log . debug (
"Node log 조회 실패" ,
e
) ;
return null ;
return null ;
}
}
}
}
/ * *
/ * *
* Run 삭 제
* KFP Run 삭 제 ( v2beta1 DELETE / apis / v2beta1 / runs / { run_id } ) .
* 성 공 ( 2 xx ) 또 는 이 미 없 음 ( 404 ) 일 때 만 정 상 반 환 . 그 외 4 xx / 5 xx 는 예 외 .
* /
* /
public void deleteKfpRun (
public void deleteKfpRun ( String runId , String experimentId ) {
String runId ,
String base = kubeflowBaseUrl . replaceAll ( "/+$" , "" ) ;
String experimentId
String url = base + "/apis/v2beta1/runs/" + runId ;
) {
if ( experimentId ! = null & & ! experimentId . isBlank ( ) ) {
url = url + "?experiment_id=" + experimentId ;
String uri =
normalizeBaseUrl ( kubeflowBaseUrl )
+ "/apis/v2beta1/runs/"
+ runId ;
if ( experimentId ! = null & &
! experimentId . isBlank ( ) ) {
uri + = "?experiment_id="
+ experimentId ;
}
}
try {
webClient . delete ( )
webClient . delete ( )
. uri ( uri )
. uri ( url )
. retrieve ( )
. retrieve ( )
. onStatus (
. onStatus ( status - > status . value ( ) = = 404 , resp - > Mono . empty ( ) )
status - >
. onStatus ( status - > status . is4xxClientError ( ) | | status . is5xxServerError ( ) ,
status . is4xxClientError ( )
resp - > resp . bodyToMono ( String . class )
| | status . is5xxServerError ( ) ,
. doOnNext ( msg - > log . warn ( "[KFP] DeleteRun error {}: {}" , resp . statusCode ( ) , msg ) )
response - >
. map ( msg - > new RuntimeException ( "KFP DeleteRun failed: " + resp . statusCode ( ) + " " + msg ) ) )
response . bodyToMono ( String . class )
. map ( msg - >
new RuntimeException (
"KFP DeleteRun failed: "
+ msg
)
)
)
. toBodilessEntity ( )
. toBodilessEntity ( )
. block ( ) ;
. block ( ) ;
} catch ( Exception e ) {
if ( e . getCause ( ) instanceof RuntimeException ) {
throw ( RuntimeException ) e . getCause ( ) ;
}
}
throw new RuntimeException ( "KFP Run 삭제 실패: " + runId , e ) ;
/ * *
* Base URL normalize
* /
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 Optional < String > optional (
String value
) {
if ( value = = null | |
value . isBlank ( ) ) {
return Optional . empty ( ) ;
}
return Optional . of ( value ) ;
}
/ * *
* Multipart Resource
* /
private static class MultipartInputStreamFileResource
extends InputStreamResource {
private final String filename ;
public MultipartInputStreamFileResource (
java . io . InputStream inputStream ,
String filename
) {
super ( inputStream ) ;
this . filename = filename ;
}
@Override
public String getFilename ( ) {
return filename ;
}
@Override
public long contentLength ( ) {
return - 1 ;
}
}
}
}
}
}