gcloud_sdk/
api_client.rs

1use std::marker::PhantomData;
2use std::time::Duration;
3
4use crate::token_source::auth_token_generator::GoogleAuthTokenGenerator;
5use async_trait::async_trait;
6use once_cell::sync::Lazy;
7use tonic::transport::Channel;
8use tower::ServiceBuilder;
9use tracing::*;
10
11use crate::middleware::{GoogleAuthMiddlewareLayer, GoogleAuthMiddlewareService};
12use crate::token_source::credentials::CredentialsInfo;
13use crate::token_source::*;
14
15#[async_trait]
16pub trait GoogleApiClientBuilder<C>
17where
18    C: Clone + Send,
19{
20    fn create_client(&self, channel: GoogleAuthMiddlewareService<Channel>) -> C;
21}
22
23#[derive(Clone)]
24pub struct GoogleApiClient<B, C>
25where
26    B: GoogleApiClientBuilder<C>,
27    C: Clone + Send,
28{
29    builder: B,
30    service: GoogleAuthMiddlewareService<Channel>,
31    _ph: PhantomData<C>,
32}
33
34impl<B, C> GoogleApiClient<B, C>
35where
36    B: GoogleApiClientBuilder<C>,
37    C: Clone + Send,
38{
39    pub async fn with_token_source<S: AsRef<str>>(
40        builder: B,
41        google_api_url: S,
42        cloud_resource_prefix: Option<String>,
43        token_source_type: TokenSourceType,
44        token_scopes: Vec<String>,
45    ) -> crate::error::Result<Self> {
46        debug!(
47            "Creating a new Google API client for {}. Scopes: {:?}",
48            google_api_url.as_ref(),
49            token_scopes
50        );
51
52        let channel = GoogleEnvironment::init_google_services_channel(google_api_url).await?;
53
54        let token_generator =
55            GoogleAuthTokenGenerator::new(token_source_type, token_scopes).await?;
56
57        let middleware = GoogleAuthMiddlewareLayer::new(token_generator, cloud_resource_prefix);
58
59        let service: GoogleAuthMiddlewareService<Channel> =
60            ServiceBuilder::new().layer(middleware).service(channel);
61
62        Ok(Self {
63            builder,
64            service,
65            _ph: PhantomData::default(),
66        })
67    }
68
69    pub fn get(&self) -> C {
70        self.builder.create_client(self.service.clone())
71    }
72}
73
74#[derive(Clone)]
75pub struct GoogleApiClientBuilderFunction<C>
76where
77    C: Clone + Send,
78{
79    f: fn(GoogleAuthMiddlewareService<Channel>) -> C,
80}
81
82impl<C> GoogleApiClientBuilder<C> for GoogleApiClientBuilderFunction<C>
83where
84    C: Clone + Send,
85{
86    fn create_client(&self, channel: GoogleAuthMiddlewareService<Channel>) -> C {
87        (self.f)(channel)
88    }
89}
90
91impl<C> GoogleApiClient<GoogleApiClientBuilderFunction<C>, C>
92where
93    C: Clone + Send,
94{
95    pub async fn from_function<S: AsRef<str>>(
96        builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
97        google_api_url: S,
98        cloud_resource_prefix_meta: Option<String>,
99    ) -> crate::error::Result<Self> {
100        Self::from_function_with_scopes(
101            builder_fn,
102            google_api_url,
103            cloud_resource_prefix_meta,
104            GCP_DEFAULT_SCOPES.clone(),
105        )
106        .await
107    }
108
109    pub async fn from_function_with_scopes<S: AsRef<str>>(
110        builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
111        google_api_url: S,
112        cloud_resource_prefix_meta: Option<String>,
113        token_scopes: Vec<String>,
114    ) -> crate::error::Result<Self> {
115        Self::from_function_with_token_source(
116            builder_fn,
117            google_api_url,
118            cloud_resource_prefix_meta,
119            token_scopes,
120            TokenSourceType::Default,
121        )
122        .await
123    }
124
125    pub async fn from_function_with_token_source<S: AsRef<str>>(
126        builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
127        google_api_url: S,
128        cloud_resource_prefix_meta: Option<String>,
129        token_scopes: Vec<String>,
130        token_source_type: TokenSourceType,
131    ) -> crate::error::Result<Self> {
132        let builder: GoogleApiClientBuilderFunction<C> =
133            GoogleApiClientBuilderFunction { f: builder_fn };
134
135        Self::with_token_source(
136            builder,
137            google_api_url,
138            cloud_resource_prefix_meta,
139            token_source_type,
140            token_scopes,
141        )
142        .await
143    }
144}
145
146pub type GoogleAuthMiddleware = GoogleAuthMiddlewareService<Channel>;
147pub type GoogleApi<C> = GoogleApiClient<GoogleApiClientBuilderFunction<C>, C>;
148
149pub struct GoogleEnvironment;
150
151impl GoogleEnvironment {
152    pub async fn detect_google_project_id() -> Option<String> {
153        let for_env = std::env::var("GCP_PROJECT")
154            .ok()
155            .or_else(|| std::env::var("PROJECT_ID").ok())
156            .or_else(|| std::env::var("GCP_PROJECT_ID").ok());
157        if for_env.is_some() {
158            debug!("Detected GCP Project ID using environment variables");
159            for_env
160        } else {
161            let local_creds = match crate::token_source::from_env_var(&GCP_DEFAULT_SCOPES) {
162                Ok(Some(creds)) => Some(creds),
163                Ok(None) | Err(_) => crate::token_source::from_well_known_file(&GCP_DEFAULT_SCOPES)
164                    .ok()
165                    .flatten(),
166            };
167
168            let local_quota_project_id =
169                local_creds.and_then(|creds| creds.quota_project_id().map(ToString::to_string));
170
171            if local_quota_project_id.is_some() {
172                debug!("Detected default project id from local defined in quota_project_id for the service account file.");
173                local_quota_project_id
174            } else {
175                let mut metadata_server =
176                    crate::token_source::metadata::Metadata::new(GCP_DEFAULT_SCOPES.clone());
177                if metadata_server.init().await {
178                    let metadata_result = metadata_server.detect_google_project_id().await;
179                    if metadata_result.is_some() {
180                        debug!("Detected GCP Project ID using GKE metadata server");
181                        metadata_result
182                    } else {
183                        debug!("No GCP Project ID detected in this environment. Please specify it explicitly using environment variables: `PROJECT_ID`,`GCP_PROJECT_ID`, or `GCP_PROJECT`");
184                        metadata_result
185                    }
186                } else {
187                    debug!("No GCP Project ID detected in this environment. Please specify it explicitly using environment variables: `PROJECT_ID`,`GCP_PROJECT_ID`, or `GCP_PROJECT`");
188                    None
189                }
190            }
191        }
192    }
193
194    pub async fn find_default_creds(
195        token_scopes: &[String],
196    ) -> crate::error::Result<Option<CredentialsInfo>> {
197        debug!("Finding default credentials for scopes: {:?}", token_scopes);
198
199        if let Some(src) = from_env_var(token_scopes)? {
200            debug!("Creating credentials based on environment variable: GOOGLE_APPLICATION_CREDENTIALS");
201            return Ok(src.to_credentials_info());
202        }
203        if let Some(src) = from_well_known_file(token_scopes)? {
204            debug!("Creating credentials based on standard config files such as application_default_credentials.json");
205            return Ok(src.to_credentials_info());
206        }
207        let mut metadata_server = crate::token_source::metadata::Metadata::new(token_scopes);
208        if metadata_server.init().await {
209            let metadata_result_email = metadata_server.email().await;
210            if let Some(email) = metadata_result_email {
211                debug!("Detected SA email using GKE metadata server");
212                return Ok(Some(CredentialsInfo {
213                    client_email: email,
214                    project_id: metadata_server.detect_google_project_id().await,
215                }));
216            }
217        }
218        Ok(None)
219    }
220
221    pub async fn init_google_services_channel<S: AsRef<str>>(
222        api_url: S,
223    ) -> Result<Channel, crate::error::Error> {
224        let api_url_string = api_url.as_ref().to_string();
225        let base_config = Channel::from_shared(api_url_string.clone())?
226            .connect_timeout(Duration::from_secs(30))
227            .tcp_keepalive(Some(Duration::from_secs(60)))
228            .keep_alive_timeout(Duration::from_secs(60))
229            .http2_keep_alive_interval(Duration::from_secs(60))
230            .keep_alive_while_idle(true);
231
232        let config = if !&api_url_string.contains("http://") {
233            let domain_name = api_url_string.replace("https://", "");
234
235            let tls_config = Self::init_tls_config(domain_name);
236            base_config.tls_config(tls_config)?
237        } else {
238            base_config
239        };
240
241        Ok(config.connect().await?)
242    }
243
244    #[cfg(not(any(feature = "tls-roots", feature = "tls-webpki-roots")))]
245    fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
246        tonic::transport::ClientTlsConfig::new()
247            .ca_certificate(tonic::transport::Certificate::from_pem(
248                crate::apis::CERTIFICATES,
249            ))
250            .domain_name(domain_name)
251    }
252
253    #[cfg(feature = "tls-roots")]
254    fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
255        tonic::transport::ClientTlsConfig::new()
256            .with_native_roots()
257            .domain_name(domain_name)
258    }
259
260    #[cfg(feature = "tls-webpki-roots")]
261    fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
262        tonic::transport::ClientTlsConfig::new()
263            .with_webpki_roots()
264            .domain_name(domain_name)
265    }
266}
267
268pub static GCP_DEFAULT_SCOPES: Lazy<Vec<String>> =
269    Lazy::new(|| vec!["https://www.googleapis.com/auth/cloud-platform".into()]);