swarmctl: agent watch follows an agent's terminal rows
Reads GET /api/agents/{name}/term/stream over the controller socket or the
gateway and prints one line per row. The gateway path uses a client without
the total-request timeout so the stream isn't cut after 30 seconds.
Closes #6160
This commit is contained in:
parent
aa227a95c1
commit
9e0c51052b
1 changed files with 482 additions and 7 deletions
|
|
@ -80,11 +80,24 @@ impl Swarm {
|
|||
/// An HTTP client that never follows a redirect: from the gateway one
|
||||
/// means "log in", and following it would land on the login page.
|
||||
pub(crate) fn http(&self) -> Result<reqwest::Client> {
|
||||
let mut builder = bounded(
|
||||
self.build_client(bounded(
|
||||
reqwest::Client::builder(),
|
||||
HTTP_CONNECT_TIMEOUT,
|
||||
HTTP_TIMEOUT,
|
||||
);
|
||||
))
|
||||
}
|
||||
|
||||
/// [`Swarm::http`] without the total-request bound, which would cut a
|
||||
/// stream off after `HTTP_TIMEOUT`. Reaching the gateway is still bounded.
|
||||
fn stream_http(&self) -> Result<reqwest::Client> {
|
||||
self.build_client(
|
||||
reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.connect_timeout(HTTP_CONNECT_TIMEOUT),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_client(&self, mut builder: reqwest::ClientBuilder) -> Result<reqwest::Client> {
|
||||
if let Some(path) = &self.ca_file {
|
||||
let pem = fs::read(path)
|
||||
.with_context(|| format!("reading the CA certificate {}", path.display()))?;
|
||||
|
|
@ -125,6 +138,21 @@ impl Swarm {
|
|||
.await
|
||||
}
|
||||
|
||||
/// `GET uri` with the cached login, answered as soon as the headers arrive
|
||||
/// so the caller can read the body as it comes. Auth failures are handled
|
||||
/// as for [`Swarm::request`]; any other status is the caller's to judge.
|
||||
pub(crate) async fn open_stream(&self, uri: &str, what: &str) -> Result<reqwest::Response> {
|
||||
self.send_authed(
|
||||
&self.stream_http()?,
|
||||
hyper::Method::GET,
|
||||
uri,
|
||||
None,
|
||||
what,
|
||||
Some("text/event-stream"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn request_with(
|
||||
&self,
|
||||
http: &reqwest::Client,
|
||||
|
|
@ -133,9 +161,29 @@ impl Swarm {
|
|||
body: Option<Vec<u8>>,
|
||||
what: &str,
|
||||
) -> Result<(hyper::StatusCode, bytes::Bytes)> {
|
||||
let resp = self
|
||||
.send_authed(http, method, uri, body, what, None)
|
||||
.await?;
|
||||
let status = resp.status();
|
||||
let body = resp.bytes().await.context("reading the response")?;
|
||||
Ok((status, body))
|
||||
}
|
||||
|
||||
async fn send_authed(
|
||||
&self,
|
||||
http: &reqwest::Client,
|
||||
method: hyper::Method,
|
||||
uri: &str,
|
||||
body: Option<Vec<u8>>,
|
||||
what: &str,
|
||||
accept: Option<&str>,
|
||||
) -> Result<reqwest::Response> {
|
||||
let url = self.origin.join(uri).context("joining the request path")?;
|
||||
let send = |token: String| {
|
||||
let mut req = http.request(method.clone(), url.clone()).bearer_auth(token);
|
||||
if let Some(accept) = accept {
|
||||
req = req.header(reqwest::header::ACCEPT, accept);
|
||||
}
|
||||
if let Some(body) = &body {
|
||||
req = req
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
|
|
@ -165,8 +213,7 @@ impl Swarm {
|
|||
self.origin
|
||||
);
|
||||
}
|
||||
let body = resp.bytes().await.context("reading the response")?;
|
||||
Ok((status, body))
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
/// The cached access token, refreshed first if it is about to expire or is
|
||||
|
|
@ -203,7 +250,7 @@ impl Swarm {
|
|||
}
|
||||
|
||||
/// `builder` following no redirect, with every request bounded by `connect`
|
||||
/// and `total`. No caller streams a response, so a total bound fits them all.
|
||||
/// and `total`. Not for a streamed response: `stream_http` has no total bound.
|
||||
fn bounded(
|
||||
builder: reqwest::ClientBuilder,
|
||||
connect: Duration,
|
||||
|
|
|
|||
Loading…
Reference in a new issue