This is an automated email from the ASF dual-hosted git repository. xuanwo pushed a commit to branch xuanwo/reqsign-google-vm-metadata-service-account in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git
commit d762f8a9f4a49e499985e1e5448e05d7f6c4a5b7 Author: Xuanwo <[email protected]> AuthorDate: Wed Mar 18 17:12:58 2026 +0800 feat(google): support selecting VM metadata service account --- services/google/src/provide_credential/default.rs | 54 +++++++++++++- .../google/src/provide_credential/vm_metadata.rs | 82 +++++++++++++++++++++- 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/services/google/src/provide_credential/default.rs b/services/google/src/provide_credential/default.rs index cf5d570..70c0b0a 100644 --- a/services/google/src/provide_credential/default.rs +++ b/services/google/src/provide_credential/default.rs @@ -264,8 +264,9 @@ impl DefaultCredentialProviderBuilder { /// Configure the VM metadata provider. /// - /// This allows setting a custom endpoint or other options for retrieving - /// tokens when running on Google Compute Engine or compatible environments. + /// This allows setting a custom endpoint, service account, or other options + /// for retrieving tokens when running on Google Compute Engine or + /// compatible environments. pub fn configure_vm_metadata<F>(mut self, f: F) -> Self where F: FnOnce(VmMetadataCredentialProvider) -> VmMetadataCredentialProvider, @@ -334,9 +335,32 @@ impl DefaultCredentialProviderBuilder { #[cfg(test)] mod tests { use super::*; + use bytes::Bytes; + use reqsign_core::HttpSend; use reqsign_core::{Context, StaticEnv}; use std::collections::HashMap; use std::env; + use std::sync::{Arc, Mutex}; + + #[derive(Clone, Debug, Default)] + struct MockHttpSend { + uris: Arc<Mutex<Vec<String>>>, + } + + impl HttpSend for MockHttpSend { + async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> { + self.uris.lock().unwrap().push(req.uri().to_string()); + + Ok(http::Response::builder() + .status(http::StatusCode::OK) + .body( + br#"{"access_token":"test-access-token","expires_in":3600}"# + .as_slice() + .into(), + ) + .expect("response must build")) + } + } #[tokio::test] async fn test_default_provider_env() { @@ -381,4 +405,30 @@ mod tests { .with_http_send(reqsign_http_send_reqwest::ReqwestHttpSend::default()); let _ = provider.provide_credential(&ctx).await; } + + #[tokio::test] + async fn test_default_provider_configures_vm_metadata_service_account() -> Result<()> { + let http = MockHttpSend::default(); + let ctx = Context::new().with_http_send(http.clone()); + + let provider = DefaultCredentialProvider::builder() + .configure_vm_metadata(|p| { + p.with_endpoint("127.0.0.1:8080") + .with_service_account("[email protected]") + }) + .build(); + + let cred = provider + .provide_credential(&ctx) + .await? + .expect("credential must exist"); + + assert!(cred.has_token()); + assert_eq!( + http.uris.lock().unwrap().as_slice(), + &["http://127.0.0.1:8080/computeMetadata/v1/instance/service-accounts/[email protected]/token?scopes=https://www.googleapis.com/auth/cloud-platform".to_string()] + ); + + Ok(()) + } } diff --git a/services/google/src/provide_credential/vm_metadata.rs b/services/google/src/provide_credential/vm_metadata.rs index b850e2d..57af156 100644 --- a/services/google/src/provide_credential/vm_metadata.rs +++ b/services/google/src/provide_credential/vm_metadata.rs @@ -35,6 +35,7 @@ struct VmMetadataTokenResponse { pub struct VmMetadataCredentialProvider { scope: Option<String>, endpoint: Option<String>, + service_account: Option<String>, } impl VmMetadataCredentialProvider { @@ -54,6 +55,14 @@ impl VmMetadataCredentialProvider { self.endpoint = Some(endpoint.into()); self } + + /// Set the service account used to retrieve a token from VM metadata service. + /// + /// Defaults to `default` if not configured. + pub fn with_service_account(mut self, service_account: impl Into<String>) -> Self { + self.service_account = Some(service_account.into()); + self + } } impl ProvideCredential for VmMetadataCredentialProvider { type Credential = Credential; @@ -66,8 +75,7 @@ impl ProvideCredential for VmMetadataCredentialProvider { .or_else(|| ctx.env_var(crate::constants::GOOGLE_SCOPE)) .unwrap_or_else(|| crate::constants::DEFAULT_SCOPE.to_string()); - // Use "default" service account if not specified - let service_account = "default"; + let service_account = self.service_account.as_deref().unwrap_or("default"); debug!("loading token from VM metadata service for account: {service_account}"); @@ -112,3 +120,73 @@ impl ProvideCredential for VmMetadataCredentialProvider { }))) } } + +#[cfg(test)] +mod tests { + use super::*; + use bytes::Bytes; + use reqsign_core::HttpSend; + use std::sync::{Arc, Mutex}; + + #[derive(Clone, Debug, Default)] + struct MockHttpSend { + uris: Arc<Mutex<Vec<String>>>, + } + + impl HttpSend for MockHttpSend { + async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> { + self.uris.lock().unwrap().push(req.uri().to_string()); + + Ok(http::Response::builder() + .status(http::StatusCode::OK) + .body( + br#"{"access_token":"test-access-token","expires_in":3600}"# + .as_slice() + .into(), + ) + .expect("response must build")) + } + } + + #[tokio::test] + async fn test_vm_metadata_uses_default_service_account() -> Result<()> { + let http = MockHttpSend::default(); + let ctx = Context::new().with_http_send(http.clone()); + + let provider = VmMetadataCredentialProvider::new().with_endpoint("127.0.0.1:8080"); + let cred = provider + .provide_credential(&ctx) + .await? + .expect("credential must exist"); + + assert!(cred.has_token()); + assert_eq!( + http.uris.lock().unwrap().as_slice(), + &["http://127.0.0.1:8080/computeMetadata/v1/instance/service-accounts/default/token?scopes=https://www.googleapis.com/auth/cloud-platform".to_string()] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_vm_metadata_uses_configured_service_account() -> Result<()> { + let http = MockHttpSend::default(); + let ctx = Context::new().with_http_send(http.clone()); + + let provider = VmMetadataCredentialProvider::new() + .with_endpoint("127.0.0.1:8080") + .with_service_account("[email protected]"); + let cred = provider + .provide_credential(&ctx) + .await? + .expect("credential must exist"); + + assert!(cred.has_token()); + assert_eq!( + http.uris.lock().unwrap().as_slice(), + &["http://127.0.0.1:8080/computeMetadata/v1/instance/service-accounts/[email protected]/token?scopes=https://www.googleapis.com/auth/cloud-platform".to_string()] + ); + + Ok(()) + } +}
