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(())
+    }
+}

Reply via email to