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