Skip to content

Commit 74e16d4

Browse files
committed
feat: add credentials driver client and --credentials-driver-socket wiring
Add CredentialsDriverHandle that connects to an out-of-process credentials driver over UDS. The handle provides resolve_credential() and list_credentials() methods wrapping the CredentialsDriver gRPC contract. When --credentials-driver-socket is set, the gateway connects at startup and stores the handle in ServerState for use by handlers. Ref: kagenti/kagenti#1353 Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
1 parent 5fbdf21 commit 74e16d4

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 Kagenti Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Credentials driver client.
5+
//!
6+
//! When `--credentials-driver-socket` is set, the gateway delegates credential
7+
//! resolution to an out-of-process driver over a Unix domain socket. The driver
8+
//! implements the `CredentialsDriver` gRPC contract defined in
9+
//! `proto/credentials_driver.proto`.
10+
11+
use openshell_core::proto::credentials::v1::credentials_driver_client::CredentialsDriverClient;
12+
use openshell_core::proto::credentials::v1::{
13+
ListCredentialsRequest, ListCredentialsResponse, ResolveCredentialRequest,
14+
ResolveCredentialResponse,
15+
};
16+
use openshell_core::{Error, Result};
17+
use std::path::Path;
18+
use std::sync::Arc;
19+
use tonic::transport::Channel;
20+
use tracing::info;
21+
22+
#[cfg(unix)]
23+
use {
24+
hyper_util::rt::TokioIo,
25+
std::time::Duration,
26+
tokio::net::UnixStream,
27+
tonic::transport::Endpoint,
28+
tower::service_fn,
29+
};
30+
31+
/// Handle to an out-of-process credentials driver connected over UDS.
32+
#[derive(Clone)]
33+
pub struct CredentialsDriverHandle {
34+
channel: Channel,
35+
}
36+
37+
impl CredentialsDriverHandle {
38+
/// Connect to a credentials driver at the given Unix domain socket path.
39+
pub async fn connect(socket_path: &Path) -> Result<Self> {
40+
let channel = connect_uds(socket_path).await?;
41+
info!(
42+
socket = %socket_path.display(),
43+
"Connected to credentials driver"
44+
);
45+
Ok(Self { channel })
46+
}
47+
48+
/// Resolve a named credential to an access token.
49+
pub async fn resolve_credential(
50+
&self,
51+
name: &str,
52+
) -> Result<ResolveCredentialResponse> {
53+
let mut client = CredentialsDriverClient::new(self.channel.clone());
54+
let response = client
55+
.resolve_credential(tonic::Request::new(ResolveCredentialRequest {
56+
name: name.to_string(),
57+
}))
58+
.await
59+
.map_err(|s| {
60+
Error::execution(format!(
61+
"credentials driver ResolveCredential failed for '{}': {}",
62+
name, s
63+
))
64+
})?;
65+
Ok(response.into_inner())
66+
}
67+
68+
/// List all available credential names.
69+
pub async fn list_credentials(&self) -> Result<ListCredentialsResponse> {
70+
let mut client = CredentialsDriverClient::new(self.channel.clone());
71+
let response = client
72+
.list_credentials(tonic::Request::new(ListCredentialsRequest {}))
73+
.await
74+
.map_err(|s| {
75+
Error::execution(format!(
76+
"credentials driver ListCredentials failed: {}",
77+
s
78+
))
79+
})?;
80+
Ok(response.into_inner())
81+
}
82+
}
83+
84+
impl std::fmt::Debug for CredentialsDriverHandle {
85+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86+
f.debug_struct("CredentialsDriverHandle")
87+
.finish_non_exhaustive()
88+
}
89+
}
90+
91+
#[cfg(unix)]
92+
async fn connect_uds(socket_path: &Path) -> Result<Channel> {
93+
let mut last_error: Option<String> = None;
94+
for _ in 0..100 {
95+
match connect_once(socket_path).await {
96+
Ok(channel) => return Ok(channel),
97+
Err(err) => last_error = Some(err.to_string()),
98+
}
99+
tokio::time::sleep(Duration::from_millis(100)).await;
100+
}
101+
102+
Err(Error::execution(format!(
103+
"timed out waiting for credentials driver socket '{}': {}",
104+
socket_path.display(),
105+
last_error.unwrap_or_else(|| "unknown error".to_string())
106+
)))
107+
}
108+
109+
#[cfg(unix)]
110+
async fn connect_once(socket_path: &Path) -> Result<Channel> {
111+
let socket_path = socket_path.to_path_buf();
112+
let display_path = socket_path.clone();
113+
Endpoint::from_static("http://[::]:50052")
114+
.connect_with_connector(service_fn(move |_: tonic::transport::Uri| {
115+
let socket_path = socket_path.clone();
116+
async move { UnixStream::connect(socket_path).await.map(TokioIo::new) }
117+
}))
118+
.await
119+
.map_err(|e| {
120+
Error::execution(format!(
121+
"failed to connect to credentials driver socket '{}': {e}",
122+
display_path.display()
123+
))
124+
})
125+
}
126+
127+
#[cfg(not(unix))]
128+
async fn connect_uds(_socket_path: &Path) -> Result<Channel> {
129+
Err(Error::config(
130+
"the credentials driver requires unix domain socket support",
131+
))
132+
}
133+
134+
/// Optional credentials driver, wrapped in Arc for sharing across handlers.
135+
pub type SharedCredentialsDriver = Option<Arc<CredentialsDriverHandle>>;

crates/openshell-server/src/lib.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
mod auth;
2323
pub mod cli;
2424
mod compute;
25+
pub mod credentials;
2526
mod grpc;
2627
mod http;
2728
mod inference;
@@ -45,6 +46,7 @@ use tokio::net::TcpListener;
4546
use tracing::{debug, error, info};
4647

4748
use compute::{ComputeRuntime, VmComputeConfig};
49+
use credentials::SharedCredentialsDriver;
4850
pub use grpc::OpenShellService;
4951
pub use http::{health_router, http_router, metrics_router};
5052
pub use multiplex::{MultiplexService, MultiplexedService};
@@ -93,6 +95,9 @@ pub struct ServerState {
9395

9496
/// OIDC JWKS cache for JWT validation. `None` when OIDC is not configured.
9597
pub oidc_cache: Option<Arc<auth::oidc::JwksCache>>,
98+
99+
/// Optional out-of-process credentials driver.
100+
pub credentials_driver: SharedCredentialsDriver,
96101
}
97102

98103
fn is_benign_tls_handshake_failure(error: &std::io::Error) -> bool {
@@ -114,6 +119,7 @@ impl ServerState {
114119
tracing_log_bus: TracingLogBus,
115120
supervisor_sessions: Arc<supervisor_session::SupervisorSessionRegistry>,
116121
oidc_cache: Option<Arc<auth::oidc::JwksCache>>,
122+
credentials_driver: SharedCredentialsDriver,
117123
) -> Self {
118124
Self {
119125
config,
@@ -127,6 +133,7 @@ impl ServerState {
127133
settings_mutex: tokio::sync::Mutex::new(()),
128134
supervisor_sessions,
129135
oidc_cache,
136+
credentials_driver,
130137
}
131138
}
132139
}
@@ -186,6 +193,15 @@ pub async fn run_server(
186193
supervisor_sessions.clone(),
187194
)
188195
.await?;
196+
197+
let credentials_driver = if !config.credentials_driver_socket.is_empty() {
198+
let socket_path = std::path::Path::new(&config.credentials_driver_socket);
199+
let handle = credentials::CredentialsDriverHandle::connect(socket_path).await?;
200+
Some(Arc::new(handle))
201+
} else {
202+
None
203+
};
204+
189205
let state = Arc::new(ServerState::new(
190206
config.clone(),
191207
store.clone(),
@@ -195,6 +211,7 @@ pub async fn run_server(
195211
tracing_log_bus,
196212
supervisor_sessions,
197213
oidc_cache,
214+
credentials_driver,
198215
));
199216

200217
state.compute.spawn_watchers();

0 commit comments

Comments
 (0)