1use olai_http::CloudClient;
2use reqwest::IntoUrl;
3use unitycatalog_common::models::temporary_credentials::v1::TemporaryCredential;
4use unitycatalog_common::{
5 model_versions::v1::GetModelVersionRequest,
6 models::temporary_credentials::v1::{
7 GenerateTemporaryModelVersionCredentialsRequest, GenerateTemporaryPathCredentialsRequest,
8 GenerateTemporaryTableCredentialsRequest, GenerateTemporaryVolumeCredentialsRequest,
9 generate_temporary_model_version_credentials_request::Operation as MvOperation,
10 generate_temporary_path_credentials_request::Operation as PthOperation,
11 generate_temporary_table_credentials_request::Operation as TblOperation,
12 generate_temporary_volume_credentials_request::Operation as VolOperation,
13 },
14 tables::v1::GetTableRequest,
15 volumes::v1::GetVolumeRequest,
16};
17use url::Url;
18use uuid::Uuid;
19
20use crate::Result;
21use crate::codegen::tables::TableServiceClient;
22pub(super) use crate::codegen::temporary_credentials::TemporaryCredentialClient as TemporaryCredentialClientBase;
23use crate::codegen::volumes::client::VolumeServiceClient;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum TableReference {
28 Id(Uuid),
30 Name(String),
32}
33
34impl From<String> for TableReference {
35 fn from(name: String) -> Self {
36 TableReference::Name(name)
37 }
38}
39
40impl From<&str> for TableReference {
41 fn from(name: &str) -> Self {
42 TableReference::Name(name.to_string())
43 }
44}
45
46impl From<Uuid> for TableReference {
47 fn from(id: Uuid) -> Self {
48 TableReference::Id(id)
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum VolumeReference {
60 Id(Uuid),
62 Name(String),
64}
65
66impl From<String> for VolumeReference {
67 fn from(name: String) -> Self {
68 VolumeReference::Name(name)
69 }
70}
71
72impl From<&str> for VolumeReference {
73 fn from(name: &str) -> Self {
74 VolumeReference::Name(name.to_string())
75 }
76}
77
78impl From<Uuid> for VolumeReference {
79 fn from(id: Uuid) -> Self {
80 VolumeReference::Id(id)
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum TableOperation {
87 Read,
89 ReadWrite,
91}
92
93impl From<TableOperation> for i32 {
94 fn from(operation: TableOperation) -> Self {
95 match operation {
96 TableOperation::Read => TblOperation::Read as i32,
97 TableOperation::ReadWrite => TblOperation::ReadWrite as i32,
98 }
99 }
100}
101
102impl From<TableOperation> for TblOperation {
103 fn from(operation: TableOperation) -> Self {
104 match operation {
105 TableOperation::Read => TblOperation::Read,
106 TableOperation::ReadWrite => TblOperation::ReadWrite,
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum PathOperation {
114 Read,
116 ReadWrite,
118 CreateTable,
120}
121
122impl From<PathOperation> for i32 {
123 fn from(operation: PathOperation) -> Self {
124 match operation {
125 PathOperation::Read => PthOperation::PathRead as i32,
126 PathOperation::ReadWrite => PthOperation::PathReadWrite as i32,
127 PathOperation::CreateTable => PthOperation::PathCreateTable as i32,
128 }
129 }
130}
131
132impl From<PathOperation> for PthOperation {
133 fn from(operation: PathOperation) -> Self {
134 match operation {
135 PathOperation::Read => PthOperation::PathRead,
136 PathOperation::ReadWrite => PthOperation::PathReadWrite,
137 PathOperation::CreateTable => PthOperation::PathCreateTable,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum VolumeOperation {
145 Read,
147 ReadWrite,
149}
150
151impl From<VolumeOperation> for i32 {
152 fn from(operation: VolumeOperation) -> Self {
153 match operation {
154 VolumeOperation::Read => VolOperation::ReadVolume as i32,
155 VolumeOperation::ReadWrite => VolOperation::WriteVolume as i32,
156 }
157 }
158}
159
160impl From<VolumeOperation> for VolOperation {
161 fn from(operation: VolumeOperation) -> Self {
162 match operation {
163 VolumeOperation::Read => VolOperation::ReadVolume,
164 VolumeOperation::ReadWrite => VolOperation::WriteVolume,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum ModelVersionOperation {
172 Read,
174 ReadWrite,
176}
177
178impl From<ModelVersionOperation> for i32 {
179 fn from(operation: ModelVersionOperation) -> Self {
180 match operation {
181 ModelVersionOperation::Read => MvOperation::ReadModelVersion as i32,
182 ModelVersionOperation::ReadWrite => MvOperation::ReadWriteModelVersion as i32,
183 }
184 }
185}
186
187impl From<ModelVersionOperation> for MvOperation {
188 fn from(operation: ModelVersionOperation) -> Self {
189 match operation {
190 ModelVersionOperation::Read => MvOperation::ReadModelVersion,
191 ModelVersionOperation::ReadWrite => MvOperation::ReadWriteModelVersion,
192 }
193 }
194}
195
196#[derive(Clone)]
203pub struct TemporaryCredentialClient {
204 client: TemporaryCredentialClientBase,
205}
206
207impl TemporaryCredentialClient {
208 pub fn new_with_url(client: CloudClient, mut base_url: Url) -> Self {
213 if !base_url.path().ends_with('/') {
214 base_url.set_path(&format!("{}/", base_url.path()));
215 }
216 Self {
217 client: TemporaryCredentialClientBase::new(client, base_url),
218 }
219 }
220
221 pub fn new(client: TemporaryCredentialClientBase) -> Self {
223 Self { client }
224 }
225
226 async fn post_credential<R: serde::Serialize>(
241 &self,
242 path: &str,
243 request: &R,
244 ) -> Result<TemporaryCredential> {
245 let url = self.client.base_url.join(path)?;
246 let response = self
247 .client
248 .client
249 .post(url)
250 .json(request)
251 .send_raw()
252 .await
253 .map_err(crate::Error::from_api_send)?;
254 let bytes = response.bytes().await?;
255
256 let mut value: serde_json::Value = serde_json::from_slice(&bytes)?;
260 if let Some(obj) = value.as_object_mut() {
261 const ONEOF_KEYS: [&str; 10] = [
262 "aws_temp_credentials",
263 "awsTempCredentials",
264 "azure_user_delegation_sas",
265 "azureUserDelegationSas",
266 "azure_aad",
267 "azureAad",
268 "gcp_oauth_token",
269 "gcpOauthToken",
270 "r2_temp_credentials",
271 "r2TempCredentials",
272 ];
273 obj.retain(|key, v| !(v.is_null() && ONEOF_KEYS.contains(&key.as_str())));
274 }
275 Ok(serde_json::from_value(value)?)
276 }
277
278 pub async fn temporary_table_credential(
287 &self,
288 table: impl Into<TableReference>,
289 operation: TableOperation,
290 ) -> Result<(TemporaryCredential, Uuid)> {
291 let (table_id, storage_location) = match table.into() {
294 TableReference::Id(id) => (id.as_hyphenated().to_string(), None),
295 TableReference::Name(name) => {
296 let table_client = TableServiceClient::new(
297 self.client.client.clone(),
298 self.client.base_url.clone(),
299 );
300 let table_info = table_client
301 .get_table(&GetTableRequest {
302 full_name: name,
303 include_browse: Some(false),
304 include_delta_metadata: Some(false),
305 include_manifest_capabilities: Some(false),
306 ..Default::default()
307 })
308 .await?;
309 (
310 table_info.table_id.clone().unwrap_or_default(),
311 table_info.storage_location.clone(),
312 )
313 }
314 };
315 let uuid =
316 Uuid::parse_str(&table_id).map_err(unitycatalog_common::Error::InvalidIdentifier)?;
317 let mut credential = self
318 .post_credential(
319 "temporary-table-credentials",
320 &GenerateTemporaryTableCredentialsRequest {
321 table_id,
322 operation: TblOperation::from(operation).into(),
323 ..Default::default()
324 },
325 )
326 .await?;
327 backfill_credential_url(&mut credential, storage_location);
328 Ok((credential, uuid))
329 }
330
331 pub async fn temporary_path_credential(
336 &self,
337 path: impl IntoUrl,
338 operation: PathOperation,
339 dry_run: impl Into<Option<bool>>,
340 ) -> Result<(TemporaryCredential, Url)> {
341 let url = path.into_url()?;
342 Ok((
343 self.post_credential(
344 "temporary-path-credentials",
345 &GenerateTemporaryPathCredentialsRequest {
346 url: url.to_string(),
347 operation: PthOperation::from(operation).into(),
348 dry_run: dry_run.into(),
349 ..Default::default()
350 },
351 )
352 .await?,
353 url,
354 ))
355 }
356
357 pub async fn temporary_volume_credential(
374 &self,
375 volume: impl Into<VolumeReference>,
376 operation: VolumeOperation,
377 ) -> Result<(TemporaryCredential, Uuid)> {
378 let (volume_id, storage_location) = match volume.into() {
379 VolumeReference::Id(id) => (id.as_hyphenated().to_string(), None),
380 VolumeReference::Name(name) => {
381 let volume_client = VolumeServiceClient::new(
382 self.client.client.clone(),
383 self.client.base_url.clone(),
384 );
385 let info = volume_client
386 .get_volume(&GetVolumeRequest {
387 name,
388 include_browse: Some(false),
389 ..Default::default()
390 })
391 .await?;
392 (info.volume_id, Some(info.storage_location))
393 }
394 };
395 let uuid =
396 Uuid::parse_str(&volume_id).map_err(unitycatalog_common::Error::InvalidIdentifier)?;
397 let mut credential = self
398 .post_credential(
399 "temporary-volume-credentials",
400 &GenerateTemporaryVolumeCredentialsRequest {
401 volume_id,
402 operation: VolOperation::from(operation).into(),
403 ..Default::default()
404 },
405 )
406 .await?;
407 backfill_credential_url(&mut credential, storage_location);
408 Ok((credential, uuid))
409 }
410
411 pub async fn temporary_model_version_credential(
424 &self,
425 full_name: impl Into<String>,
426 version: i64,
427 operation: ModelVersionOperation,
428 ) -> Result<TemporaryCredential> {
429 let full_name = full_name.into();
430 let [catalog_name, schema_name, model_name] =
431 <[String; 3]>::try_from(full_name.split('.').map(str::to_string).collect::<Vec<_>>())
432 .map_err(|_| {
433 unitycatalog_common::Error::invalid_argument(
434 "full_name must be a three-level catalog.schema.model name",
435 )
436 })?;
437
438 let mv_client = crate::codegen::model_versions::ModelVersionServiceClient::new(
441 self.client.client.clone(),
442 self.client.base_url.clone(),
443 );
444 let storage_location = mv_client
445 .get_model_version(&GetModelVersionRequest {
446 full_name: full_name.clone(),
447 version,
448 ..Default::default()
449 })
450 .await
451 .ok()
452 .and_then(|mv| mv.storage_location);
453
454 let mut credential = self
455 .post_credential(
456 "temporary-model-version-credentials",
457 &GenerateTemporaryModelVersionCredentialsRequest {
458 catalog_name,
459 schema_name,
460 model_name,
461 version,
462 operation: MvOperation::from(operation).into(),
463 ..Default::default()
464 },
465 )
466 .await?;
467 backfill_credential_url(&mut credential, storage_location);
468 Ok(credential)
469 }
470}
471
472fn backfill_credential_url(credential: &mut TemporaryCredential, storage_location: Option<String>) {
481 if credential.url.is_empty()
482 && let Some(location) = storage_location
483 && !location.is_empty()
484 {
485 credential.url = location;
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
496 fn malformed_table_id_is_error_not_panic() {
497 let result =
498 Uuid::parse_str("not-a-uuid").map_err(unitycatalog_common::Error::InvalidIdentifier);
499 let err: crate::Error = result.unwrap_err().into();
500 assert!(matches!(err, crate::Error::Common { .. }));
501 }
502}