Skip to content

Commit 9daf064

Browse files
committed
feat(cli): support --from-gcloud-adc for google-cloud providers
Widen --from-gcloud-adc to accept google-cloud providers. The ADC credential key is derived from the provider profile rather than hardcoded per type, so future GCP provider types get ADC support by declaring the right refresh metadata in their profile YAML. Add ProviderTypeProfile::adc_credential() to find the ADC-compatible credential from a profile's refresh metadata. Remove unused VERTEX_AI_ADC_TOKEN_KEY and GCP_ADC_TOKEN_KEY constants. Signed-off-by: Robert Sturla <rsturla@redhat.com>
1 parent 65097d0 commit 9daf064

7 files changed

Lines changed: 151 additions & 44 deletions

File tree

crates/openshell-cli/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ enum ProviderCommands {
740740

741741
/// Configure credentials from gcloud Application Default Credentials
742742
/// (`~/.config/gcloud/application_default_credentials.json`).
743-
/// Only valid for google-vertex-ai providers.
743+
/// Valid for providers whose profile declares an ADC-compatible credential.
744744
#[arg(long, group = "cred_source", conflicts_with_all = ["from_existing", "credentials", "runtime_credentials"])]
745745
from_gcloud_adc: bool,
746746

crates/openshell-cli/src/run.rs

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4347,7 +4347,7 @@ fn read_gcloud_adc() -> Result<(String, String, String)> {
43474347
Ok((client_id, client_secret, refresh_token))
43484348
}
43494349

4350-
async fn rollback_provider_create_after_vertex_adc_failure(
4350+
async fn rollback_provider_create_after_gcloud_adc_failure(
43514351
client: &mut crate::tls::GrpcClient,
43524352
provider_name: &str,
43534353
stage: &str,
@@ -4360,7 +4360,7 @@ async fn rollback_provider_create_after_vertex_adc_failure(
43604360
.await
43614361
{
43624362
Ok(_) => Err(miette!(
4363-
"failed to {stage} Vertex AI credentials from gcloud ADC for provider '{provider_name}': {source}. \
4363+
"failed to {stage} credentials from gcloud ADC for provider '{provider_name}': {source}. \
43644364
The provider was rolled back successfully."
43654365
)),
43664366
Err(cleanup_err) => {
@@ -4374,7 +4374,7 @@ async fn rollback_provider_create_after_vertex_adc_failure(
43744374
provider_name
43754375
);
43764376
Err(miette!(
4377-
"failed to {stage} Vertex AI credentials from gcloud ADC for provider '{provider_name}': {source}. \
4377+
"failed to {stage} credentials from gcloud ADC for provider '{provider_name}': {source}. \
43784378
Cleanup also failed, so the provider may still exist. \
43794379
Run 'openshell provider delete {provider_name}' to remove it manually."
43804380
))
@@ -4483,6 +4483,9 @@ async fn discover_existing_provider_data(
44834483
/// Canonical provider type string for Google Vertex AI.
44844484
const VERTEX_AI_PROVIDER_TYPE: &str = "google-vertex-ai";
44854485

4486+
/// Canonical provider type string for Google Cloud (GCP APIs).
4487+
const GOOGLE_CLOUD_PROVIDER_TYPE: &str = "google-cloud";
4488+
44864489
fn missing_credentials_error(provider_type: &str) -> miette::Report {
44874490
if provider_type == VERTEX_AI_PROVIDER_TYPE {
44884491
return miette::miette!(
@@ -4493,6 +4496,14 @@ fn missing_credentials_error(provider_type: &str) -> miette::Report {
44934496
);
44944497
}
44954498

4499+
if provider_type == GOOGLE_CLOUD_PROVIDER_TYPE {
4500+
return miette::miette!(
4501+
"no credentials resolved for provider type '{provider_type}'. \
4502+
Set GCP_ADC_ACCESS_TOKEN or GCP_SA_ACCESS_TOKEN; \
4503+
or use --from-gcloud-adc / --from-existing with those env vars set."
4504+
);
4505+
}
4506+
44964507
miette::miette!(
44974508
"no credentials resolved for provider type '{provider_type}'. \
44984509
Use --credential KEY[=VALUE], --runtime-credentials for runtime-resolved profile credentials, \
@@ -4583,11 +4594,34 @@ pub async fn provider_create_with_options(
45834594
}
45844595
};
45854596

4586-
if from_gcloud_adc && provider_type != VERTEX_AI_PROVIDER_TYPE {
4587-
return Err(miette::miette!(
4588-
"--from-gcloud-adc is only valid for google-vertex-ai providers"
4589-
));
4590-
}
4597+
let adc_credential_key = if from_gcloud_adc {
4598+
let profile =
4599+
openshell_providers::get_default_profile(&provider_type).ok_or_else(|| {
4600+
miette::miette!(
4601+
"--from-gcloud-adc requires a built-in provider profile, \
4602+
but '{provider_type}' has none"
4603+
)
4604+
})?;
4605+
let adc_cred = profile.adc_credential().ok_or_else(|| {
4606+
miette::miette!(
4607+
"--from-gcloud-adc is not supported for '{provider_type}' providers \
4608+
(no ADC-compatible credential in the provider profile)"
4609+
)
4610+
})?;
4611+
Some(
4612+
adc_cred
4613+
.env_vars
4614+
.first()
4615+
.ok_or_else(|| {
4616+
miette::miette!(
4617+
"ADC credential in '{provider_type}' profile has no env_vars declared"
4618+
)
4619+
})?
4620+
.clone(),
4621+
)
4622+
} else {
4623+
None
4624+
};
45914625

45924626
let mut credential_map = parse_credential_pairs(credentials)?;
45934627
let mut config_map = parse_key_value_pairs(config, "--config")?;
@@ -4636,10 +4670,12 @@ pub async fn provider_create_with_options(
46364670
}
46374671

46384672
// Validate and read the ADC file BEFORE creating the provider so that
4639-
// a bad/missing ADC does not leave an orphan provider behind.
4640-
let gcloud_adc_material = if from_gcloud_adc {
4673+
// a bad/missing ADC does not leave an orphan provider behind. Bundle the
4674+
// credential key with the material so they stay coupled.
4675+
let gcloud_adc_bootstrap = if from_gcloud_adc {
46414676
let (client_id, client_secret, refresh_token) = read_gcloud_adc()?;
4642-
Some((client_id, client_secret, refresh_token))
4677+
let key = adc_credential_key.expect("set when from_gcloud_adc is true");
4678+
Some((key, client_id, client_secret, refresh_token))
46434679
} else {
46444680
None
46454681
};
@@ -4669,7 +4705,9 @@ pub async fn provider_create_with_options(
46694705
.ok_or_else(|| miette::miette!("provider missing from response"))?;
46704706
let provider_name = provider.object_name().to_string();
46714707

4672-
if let Some((client_id, client_secret, refresh_token)) = gcloud_adc_material {
4708+
if let Some((adc_credential_key, client_id, client_secret, refresh_token)) =
4709+
gcloud_adc_bootstrap
4710+
{
46734711
let mut material = HashMap::new();
46744712
material.insert("client_id".to_string(), client_id);
46754713
material.insert("client_secret".to_string(), client_secret);
@@ -4678,7 +4716,7 @@ pub async fn provider_create_with_options(
46784716
if let Err(configure_err) = client
46794717
.configure_provider_refresh(ConfigureProviderRefreshRequest {
46804718
provider: provider_name.clone(),
4681-
credential_key: openshell_core::inference::VERTEX_AI_ADC_TOKEN_KEY.to_string(),
4719+
credential_key: adc_credential_key.clone(),
46824720
strategy: ProviderCredentialRefreshStrategy::Oauth2RefreshToken as i32,
46834721
material,
46844722
secret_material_keys: vec![
@@ -4689,7 +4727,7 @@ pub async fn provider_create_with_options(
46894727
})
46904728
.await
46914729
{
4692-
return rollback_provider_create_after_vertex_adc_failure(
4730+
return rollback_provider_create_after_gcloud_adc_failure(
46934731
&mut client,
46944732
&provider_name,
46954733
"configure",
@@ -4701,11 +4739,11 @@ pub async fn provider_create_with_options(
47014739
if let Err(rotate_err) = client
47024740
.rotate_provider_credential(RotateProviderCredentialRequest {
47034741
provider: provider_name.clone(),
4704-
credential_key: openshell_core::inference::VERTEX_AI_ADC_TOKEN_KEY.to_string(),
4742+
credential_key: adc_credential_key,
47054743
})
47064744
.await
47074745
{
4708-
return rollback_provider_create_after_vertex_adc_failure(
4746+
return rollback_provider_create_after_gcloud_adc_failure(
47094747
&mut client,
47104748
&provider_name,
47114749
"mint the initial access token for",
@@ -4715,9 +4753,7 @@ pub async fn provider_create_with_options(
47154753
}
47164754

47174755
println!("{} Created provider {}", "✓".green().bold(), provider_name);
4718-
println!(
4719-
"Configured Vertex AI credentials from gcloud ADC and minted the initial access token"
4720-
);
4756+
println!("Configured GCP credentials from gcloud ADC and minted the initial access token");
47214757
return Ok(());
47224758
}
47234759

crates/openshell-cli/tests/provider_commands_integration.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2234,8 +2234,7 @@ async fn provider_create_from_gcloud_adc_rejects_wrong_provider_type_before_cred
22342234
.expect_err("wrong provider type should fail before generic credential validation");
22352235

22362236
assert!(
2237-
err.to_string()
2238-
.contains("--from-gcloud-adc is only valid for google-vertex-ai providers"),
2237+
err.to_string().contains("--from-gcloud-adc"),
22392238
"unexpected error: {err}"
22402239
);
22412240
assert!(ts.state.providers.lock().await.is_empty());

crates/openshell-core/src/inference.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,6 @@ pub const VERTEX_AI_CREDENTIAL_KEY_NAMES: &[&str] = &[
102102
"VERTEX_AI_TOKEN",
103103
];
104104

105-
/// The credential key used for tokens minted from gcloud Application Default Credentials.
106-
///
107-
/// This is the key written by the gateway's `OAuth2` refresh worker when using the
108-
/// `--from-gcloud-adc` CLI flow. It must match `VERTEX_AI_CREDENTIAL_KEY_NAMES[2]`.
109-
pub const VERTEX_AI_ADC_TOKEN_KEY: &str = "GOOGLE_VERTEX_AI_TOKEN";
110-
111105
/// GCP project ID config key for Vertex AI providers.
112106
pub const VERTEX_AI_PROJECT_ID_KEY: &str = "VERTEX_AI_PROJECT_ID";
113107

crates/openshell-providers/src/profiles.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,25 @@ impl ProviderTypeProfile {
371371
has_runtime_resolvable_credential
372372
}
373373

374+
/// Returns the credential suitable for `--from-gcloud-adc` bootstrap, if any.
375+
///
376+
/// A credential qualifies when its refresh strategy is `Oauth2RefreshToken`
377+
/// and its material declares the three gcloud ADC keys (`client_id`,
378+
/// `client_secret`, `refresh_token`).
379+
#[must_use]
380+
pub fn adc_credential(&self) -> Option<&CredentialProfile> {
381+
const ADC_MATERIAL_KEYS: &[&str] = &["client_id", "client_secret", "refresh_token"];
382+
383+
self.credentials.iter().find(|cred| {
384+
cred.refresh.as_ref().is_some_and(|refresh| {
385+
refresh.strategy == ProviderCredentialRefreshStrategy::Oauth2RefreshToken
386+
&& ADC_MATERIAL_KEYS
387+
.iter()
388+
.all(|key| refresh.material.iter().any(|m| m.name == *key))
389+
})
390+
})
391+
}
392+
374393
#[must_use]
375394
pub fn to_proto(&self) -> ProviderProfile {
376395
ProviderProfile {
@@ -1790,6 +1809,73 @@ credentials:
17901809
assert!(!static_only_profile.allows_empty_provider_credentials());
17911810
}
17921811

1812+
#[test]
1813+
fn adc_credential_returns_oauth2_refresh_token_credential_with_adc_material() {
1814+
let profile = get_default_profile("google-cloud").expect("google-cloud profile");
1815+
let adc = profile
1816+
.adc_credential()
1817+
.expect("google-cloud should have an ADC credential");
1818+
assert_eq!(adc.env_vars[0], "GCP_ADC_ACCESS_TOKEN");
1819+
1820+
let profile = get_default_profile("google-vertex-ai").expect("vertex profile");
1821+
let adc = profile
1822+
.adc_credential()
1823+
.expect("vertex should have an ADC credential");
1824+
assert_eq!(adc.env_vars[0], "GOOGLE_VERTEX_AI_TOKEN");
1825+
}
1826+
1827+
#[test]
1828+
fn adc_credential_returns_none_for_profiles_without_adc() {
1829+
let profile = get_default_profile("github").expect("github profile");
1830+
assert!(profile.adc_credential().is_none());
1831+
1832+
let profile = get_default_profile("claude-code").expect("claude-code profile");
1833+
assert!(profile.adc_credential().is_none());
1834+
}
1835+
1836+
#[test]
1837+
fn adc_credential_rejects_service_account_jwt_strategy() {
1838+
let profile = parse_profile_yaml(
1839+
r"
1840+
id: sa-only
1841+
display_name: SA Only
1842+
credentials:
1843+
- name: sa_token
1844+
env_vars: [SA_TOKEN]
1845+
refresh:
1846+
strategy: google_service_account_jwt
1847+
material:
1848+
- name: client_email
1849+
- name: private_key
1850+
",
1851+
)
1852+
.expect("profile");
1853+
assert!(profile.adc_credential().is_none());
1854+
}
1855+
1856+
#[test]
1857+
fn adc_credential_requires_all_three_material_keys() {
1858+
let profile = parse_profile_yaml(
1859+
r"
1860+
id: partial-material
1861+
display_name: Partial Material
1862+
credentials:
1863+
- name: token
1864+
env_vars: [TOKEN]
1865+
refresh:
1866+
strategy: oauth2_refresh_token
1867+
material:
1868+
- name: client_id
1869+
- name: client_secret
1870+
",
1871+
)
1872+
.expect("profile");
1873+
assert!(
1874+
profile.adc_credential().is_none(),
1875+
"missing refresh_token material should not qualify"
1876+
);
1877+
}
1878+
17931879
#[test]
17941880
fn parse_profile_yaml_reads_single_provider_document() {
17951881
let profile = parse_profile_yaml(

docs/providers/google-cloud.mdx

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,23 @@ on loopback provides credential placeholders that the
1414
sandbox proxy resolves to real tokens at request time. The sandbox process
1515
never holds a real GCP credential.
1616

17-
## Quick Start (Manual Token)
17+
## Quick Start
1818

19-
If you already have `gcloud` configured, you can create a provider with a
20-
short-lived access token from Application Default Credentials. This is the
21-
fastest way to test GCP access in a sandbox.
19+
If you already have `gcloud` configured with Application Default Credentials,
20+
create a provider with automatic credential refresh in one command:
2221

2322
```shell
24-
export GCP_ADC_ACCESS_TOKEN="$(gcloud auth application-default print-access-token)"
25-
2623
openshell provider create \
2724
--name my-gcp \
2825
--type google-cloud \
29-
--from-existing \
26+
--from-gcloud-adc \
3027
--config project_id="$(gcloud config get-value project)" \
3128
--config region=global
3229
```
3330

34-
<Note>
35-
The token from `gcloud auth application-default print-access-token` expires
36-
after approximately 60 minutes. To keep the provider alive beyond that,
37-
configure credential refresh using the
38-
[Application Default Credentials](#application-default-credentials-gcloud-adc)
39-
flow below. The manual token bootstraps the provider; the refresh config
40-
keeps it running.
41-
</Note>
31+
`--from-gcloud-adc` reads your ADC file, configures OAuth2 refresh on the
32+
gateway, and mints the first access token before the command returns. The
33+
gateway rotates the token automatically — no manual refresh needed.
4234

4335
## Authentication Flows
4436

docs/providers/google-vertex-ai.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ openshell provider create \
6868
ADC-backed providers mint and rotate access tokens into `GOOGLE_VERTEX_AI_TOKEN`.
6969

7070
<Note>
71-
`--from-gcloud-adc` is only valid for `google-vertex-ai` providers.
71+
`--from-gcloud-adc` is valid for `google-vertex-ai` and `google-cloud` providers.
7272
</Note>
7373

7474
## Configuration Keys

0 commit comments

Comments
 (0)