요청의 여정
pods.list() 한 줄 호출이 내부적으로 어떤 코드를 거쳐 Kubernetes API 서버에 도달하고 응답이 돌아오는지 추적합니다.
호출 코드
let client = Client::try_default().await?;
let pods: Api<Pod> = Api::default_namespaced(client);
let list = pods.list(&ListParams::default()).await?;
이 세 줄이 내부에서 거치는 전체 경로를 따라갑니다.
전체 흐름
Api<K> 내부
Api<K>는 kube-core의 URL 빌더와 Client를 연결하는 얇은 핸들입니다.
pub struct Api<K> {
request: kube_core::Request, // URL path builder
client: Client,
namespace: Option<String>,
_phantom: std::iter::Empty<K>, // PhantomData 대신 — Send 보장
}
list() 호출 시 내부 동작:
self.request.list(&lp)—http::Request<Vec<u8>>를 생성합니다.- URL이 조립됩니다:
/api/v1/namespaces/{ns}/pods?limit=...&labelSelector=... - 요청에 extension을 추가합니다 (트레이싱용으로
"list"문자열). self.client.request::<ObjectList<Pod>>(req).await로 실제 요청을 보냅니다.
kube-core::Request — URL 빌더
kube_core::Request는 URL path를 들고 있는 순수한 빌더입니다.
pub struct Request {
url_path: String, // e.g., "/api/v1/namespaces/default/pods"
}
impl Request {
pub fn list(&self, lp: &ListParams) -> Result<http::Request<Vec<u8>>> {
let url = format!("{}?{}", self.url_path, lp.as_query_string());
http::Request::builder()
.method("GET")
.uri(url)
.body(vec![])
}
}
핵심은 네트워크 전송이 전혀 없다는 것입니다. list(), get(), create(), watch() 등 각 메서드는 적절한 HTTP 메서드와 쿼리 파라미터를 조립한 http::Request만 반환합니다. 이 분리 덕분에 kube-core는 네트워크 의존성 없이 테스트할 수 있습니다.
Client를 통한 요청 실행
Client::request::<T>(req) 메서드가 실제 요청을 실행합니다.
send(req): Tower 미들웨어 스택을 통과해 HTTP 요청을 보냅니다.- 에러 처리 (
handle_api_errors): 상태 코드를 확인합니다.- 4xx/5xx: 응답 body를
Status구조체로 파싱하고Error::Api를 반환합니다. - 2xx: 정상 처리를 계속합니다.
- 4xx/5xx: 응답 body를
- 역직렬화: 응답 body를 bytes로 수집한 뒤
serde_json::from_slice::<T>()로 변환합니다.
에러 분기
| 에러 종류 | 타입 | 발생 시점 |
|---|---|---|
| 네트워크 에러 | Error::HyperError | TCP 연결 실패, DNS 해석 실패 등 |
| HTTP 에러 | Error::HttpError | HTTP 프로토콜 레벨 에러 |
| API 에러 | Error::Api { status } | Kubernetes가 반환한 4xx/5xx |
| 역직렬화 에러 | Error::SerializationError | JSON 파싱 실패 |
Error::Api의 status 필드는 Kubernetes API 서버가 보낸 구조화된 에러입니다:
let err = pods.get("nonexistent").await.unwrap_err();
if let kube::Error::Api(status) = err {
println!("code: {}", status.code); // 404
println!("reason: {}", status.reason); // "NotFound"
println!("message: {}", status.message); // "pods \"nonexistent\" not found"
}
Watch 요청의 특수성
일반 요청은 요청-응답으로 완료되지만, watch 요청은 끊기지 않는 스트림을 반환합니다.
// 일반 요청: 완료되면 끝
let list = pods.list(&lp).await?;
// Watch 요청: 무한 스트림
let mut stream = pods.watch(&WatchParams::default(), "0").await?;
while let Some(event) = stream.try_next().await? {
// WatchEvent를 하나씩 처리
}
내부 동작
Client::request_events::<T>(req)가 watch 스트림을 처리합니다:
send(req)→Response<Body>(chunked transfer encoding)- Body를
AsyncBufRead로 변환 - 줄 단위로 분리 (각 줄이 하나의 JSON 객체)
- 각 줄을
WatchEvent<T>로 역직렬화 TryStream<Item = Result<WatchEvent<T>>>를 반환
WatchEvent
API 서버는 다섯 가지 이벤트를 보냅니다:
pub enum WatchEvent<K> {
Added(K), // 리소스가 생성됨
Modified(K), // 리소스가 변경됨
Deleted(K), // 리소스가 삭제됨
Bookmark(Bookmark), // 진행 마커 (resourceVersion 갱신)
Error(Box<Status>), // API 서버가 보낸 에러 (e.g., 410 Gone)
}
Bookmark은 실제 리소스 변경이 아닙니다. API 서버가 주기적으로 현재 resourceVersion을 알려주는 마커입니다. 연결이 끊겼다가 재연결할 때 이 resourceVersion부터 watch를 재개할 수 있습니다.
Api::watch()는 raw watch 스트림입니다. 연결이 끊기면 끝나고, resourceVersion 만료 대응도 없습니다. 실전에서는 이 위에 자동 재연결과 에러 복구를 올린 kube_runtime::watcher()를 사용합니다.
watcher의 내부 동작은 Watcher 상태 머신에서 다룹니다.