Skip to content

Commit cf04411

Browse files
authored
feat: add gitlab CI for trusted publishing (#1904)
1 parent 3419042 commit cf04411

File tree

3 files changed

+224
-28
lines changed

3 files changed

+224
-28
lines changed

crates/rattler_upload/src/upload/trusted_publishing.rs

Lines changed: 193 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// This code has been adapted from uv under https://github.com/astral-sh/uv/blob/c5caf92edf539a9ebf24d375871178f8f8a0ab93/crates/uv-publish/src/trusted_publishing.rs
22
// The original code is dual-licensed under Apache-2.0 and MIT
33

4-
//! Trusted publishing (via OIDC) with GitHub actions.
4+
//! Trusted publishing (via OIDC) with GitHub Actions, GitLab CI, and Google Cloud.
55
66
use reqwest::StatusCode;
77
use reqwest_middleware::ClientWithMiddleware;
@@ -12,24 +12,57 @@ use std::ffi::OsString;
1212
use thiserror::Error;
1313
use url::Url;
1414

15-
use crate::utils::console_utils::github_action_runner;
15+
use crate::utils::console_utils::{github_action_runner, gitlab_ci_runner, google_cloud_runner};
1616
use crate::utils::consts;
1717

18+
/// Represents the CI provider being used for trusted publishing.
19+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20+
pub enum CiProvider {
21+
GitHubActions,
22+
GitLabCI,
23+
GoogleCloud,
24+
}
25+
26+
impl std::fmt::Display for CiProvider {
27+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28+
match self {
29+
CiProvider::GitHubActions => write!(f, "GitHub Actions"),
30+
CiProvider::GitLabCI => write!(f, "GitLab CI"),
31+
CiProvider::GoogleCloud => write!(f, "Google Cloud"),
32+
}
33+
}
34+
}
35+
36+
/// Detects which CI provider is being used, if any.
37+
pub fn detect_ci_provider() -> Option<CiProvider> {
38+
if github_action_runner() {
39+
Some(CiProvider::GitHubActions)
40+
} else if gitlab_ci_runner() {
41+
Some(CiProvider::GitLabCI)
42+
} else if google_cloud_runner() {
43+
Some(CiProvider::GoogleCloud)
44+
} else {
45+
None
46+
}
47+
}
48+
1849
/// If applicable, attempt obtaining a token for trusted publishing.
1950
pub async fn check_trusted_publishing(
2051
client: &ClientWithMiddleware,
2152
prefix_url: &Url,
2253
) -> TrustedPublishResult {
23-
// If we aren't in GitHub Actions, we can't use trusted publishing.
24-
if !github_action_runner() {
25-
return TrustedPublishResult::Skipped;
26-
}
27-
// We could check for credentials from the keyring or netrc the auth middleware first, but
28-
// given that we are in GitHub Actions we check for trusted publishing first.
54+
// Check which CI provider we're running on
55+
let provider = match detect_ci_provider() {
56+
Some(p) => p,
57+
None => return TrustedPublishResult::Skipped,
58+
};
59+
2960
tracing::debug!(
30-
"Running on GitHub Actions without explicit credentials, checking for trusted publishing"
61+
"Running on {} without explicit credentials, checking for trusted publishing",
62+
provider
3163
);
32-
match get_token(client, prefix_url).await {
64+
65+
match get_token(client, prefix_url, provider).await {
3366
Ok(token) => TrustedPublishResult::Configured(token),
3467
Err(err) => {
3568
tracing::debug!("Could not obtain trusted publishing credentials, skipping: {err}");
@@ -63,6 +96,14 @@ pub enum TrustedPublishingError {
6396
"Prefix.dev returned error code {0}, is trusted publishing correctly configured?\nResponse: {1}"
6497
)]
6598
PrefixDev(StatusCode, String),
99+
#[error("GitLab CI OIDC token not found. Make sure you have configured `id_tokens` in your .gitlab-ci.yml:\n\n\
100+
job_name:\n \
101+
id_tokens:\n \
102+
PREFIX_ID_TOKEN:\n \
103+
aud: prefix.dev\n")]
104+
GitLabOidcTokenNotFound,
105+
#[error("Google Cloud OIDC token retrieval failed. Make sure you are running in a Google Cloud environment (Cloud Build, Cloud Run, GCE, or GKE) with a service account attached.")]
106+
GoogleCloudOidcTokenNotFound,
66107
}
67108

68109
impl TrustedPublishingError {
@@ -100,42 +141,115 @@ struct MintTokenRequest {
100141
pub async fn get_token(
101142
client: &ClientWithMiddleware,
102143
prefix_url: &Url,
144+
provider: CiProvider,
103145
) -> Result<TrustedPublishingToken, TrustedPublishingError> {
104-
// If this fails, we can skip the audience request.
105-
let oidc_token_request_token =
106-
env::var(consts::ACTIONS_ID_TOKEN_REQUEST_TOKEN).map_err(|err| {
107-
TrustedPublishingError::from_var_err(consts::ACTIONS_ID_TOKEN_REQUEST_TOKEN, err)
108-
})?;
146+
// Get the OIDC token based on the CI provider
147+
let oidc_token = match provider {
148+
CiProvider::GitHubActions => {
149+
// If this fails, we can skip the audience request.
150+
let oidc_token_request_token = env::var(consts::ACTIONS_ID_TOKEN_REQUEST_TOKEN)
151+
.map_err(|err| {
152+
TrustedPublishingError::from_var_err(
153+
consts::ACTIONS_ID_TOKEN_REQUEST_TOKEN,
154+
err,
155+
)
156+
})?;
109157

110-
// Request 1: Get the OIDC token from GitHub.
111-
let oidc_token = get_oidc_token(&oidc_token_request_token, client).await?;
158+
// Request 1: Get the OIDC token from GitHub.
159+
get_github_oidc_token(&oidc_token_request_token, client).await?
160+
}
161+
CiProvider::GitLabCI => {
162+
// Get the OIDC token from GitLab CI environment variable
163+
get_gitlab_oidc_token()?
164+
}
165+
CiProvider::GoogleCloud => {
166+
// Get the OIDC token from Google Cloud metadata server
167+
get_google_cloud_oidc_token(client).await?
168+
}
169+
};
112170

113171
// Request 2: Get the publishing token from prefix.dev.
114172
let publish_token = get_publish_token(&oidc_token, prefix_url, client).await?;
115173

116-
tracing::info!("Received token, using trusted publishing");
174+
tracing::info!("Received token from {}, using trusted publishing", provider);
117175

118-
// Tell GitHub Actions to mask the token in any console logs.
119-
if github_action_runner() {
120-
println!("::add-mask::{}", &publish_token.secret());
176+
// Mask the token in CI logs
177+
match provider {
178+
CiProvider::GitHubActions => {
179+
println!("::add-mask::{}", &publish_token.secret());
180+
}
181+
CiProvider::GitLabCI => {
182+
// GitLab CI doesn't have a built-in mask mechanism like GitHub Actions,
183+
// but the token should be short-lived anyway
184+
tracing::debug!("Token obtained via GitLab CI trusted publishing");
185+
}
186+
CiProvider::GoogleCloud => {
187+
// Google Cloud doesn't have a built-in mask mechanism,
188+
// but the token should be short-lived anyway
189+
tracing::debug!("Token obtained via Google Cloud trusted publishing");
190+
}
121191
}
122192

123193
Ok(publish_token)
124194
}
125195

126-
/// Get raw OIDC token for attestation generation
196+
/// Get raw OIDC token for attestation generation.
197+
/// Returns the OIDC token from the current CI provider.
127198
pub async fn get_raw_oidc_token(
128199
client: &ClientWithMiddleware,
129200
) -> Result<String, TrustedPublishingError> {
130-
let oidc_token_request_token =
131-
env::var(consts::ACTIONS_ID_TOKEN_REQUEST_TOKEN).map_err(|err| {
132-
TrustedPublishingError::from_var_err(consts::ACTIONS_ID_TOKEN_REQUEST_TOKEN, err)
133-
})?;
201+
match detect_ci_provider() {
202+
Some(CiProvider::GitHubActions) => {
203+
let oidc_token_request_token = env::var(consts::ACTIONS_ID_TOKEN_REQUEST_TOKEN)
204+
.map_err(|err| {
205+
TrustedPublishingError::from_var_err(
206+
consts::ACTIONS_ID_TOKEN_REQUEST_TOKEN,
207+
err,
208+
)
209+
})?;
210+
get_github_oidc_token(&oidc_token_request_token, client).await
211+
}
212+
Some(CiProvider::GitLabCI) => get_gitlab_oidc_token(),
213+
Some(CiProvider::GoogleCloud) => get_google_cloud_oidc_token(client).await,
214+
None => Err(TrustedPublishingError::MissingEnvVar(
215+
"GITHUB_ACTIONS, GITLAB_CI, or CLOUD_BUILD_ID/K_SERVICE",
216+
)),
217+
}
218+
}
134219

135-
get_oidc_token(&oidc_token_request_token, client).await
220+
/// Get the OIDC token from GitLab CI.
221+
/// GitLab CI provides the token via the `PREFIX_ID_TOKEN` environment variable
222+
/// when configured with `id_tokens` in the `.gitlab-ci.yml` file.
223+
fn get_gitlab_oidc_token() -> Result<String, TrustedPublishingError> {
224+
// GitLab CI provides OIDC tokens via the `id_tokens` keyword in .gitlab-ci.yml
225+
// The user should configure their job like this:
226+
//
227+
// job_name:
228+
// id_tokens:
229+
// PREFIX_ID_TOKEN:
230+
// aud: prefix.dev
231+
// script:
232+
// - rattler upload ...
233+
//
234+
// The token is then available as the PREFIX_ID_TOKEN environment variable.
235+
match env::var(consts::PREFIX_ID_TOKEN) {
236+
Ok(token) if !token.is_empty() => {
237+
tracing::info!("Found GitLab CI OIDC token in PREFIX_ID_TOKEN");
238+
Ok(token)
239+
}
240+
Ok(_) => {
241+
tracing::warn!("PREFIX_ID_TOKEN is set but empty");
242+
Err(TrustedPublishingError::GitLabOidcTokenNotFound)
243+
}
244+
Err(_) => {
245+
tracing::debug!("PREFIX_ID_TOKEN not found in environment");
246+
Err(TrustedPublishingError::GitLabOidcTokenNotFound)
247+
}
248+
}
136249
}
137250

138-
async fn get_oidc_token(
251+
/// Get the OIDC token from GitHub Actions.
252+
async fn get_github_oidc_token(
139253
oidc_token_request_token: &str,
140254
client: &ClientWithMiddleware,
141255
) -> Result<String, TrustedPublishingError> {
@@ -162,6 +276,57 @@ async fn get_oidc_token(
162276
Ok(oidc_token.value)
163277
}
164278

279+
/// Get the OIDC token from Google Cloud metadata server.
280+
/// Works in Cloud Build, Cloud Run, GCE, and GKE with Workload Identity.
281+
/// Respects the `GCE_METADATA_HOST` environment variable for custom metadata server hostnames.
282+
async fn get_google_cloud_oidc_token(
283+
client: &ClientWithMiddleware,
284+
) -> Result<String, TrustedPublishingError> {
285+
// Use GCE_METADATA_HOST if set, otherwise use the default hostname
286+
let metadata_host = env::var(consts::GCE_METADATA_HOST)
287+
.unwrap_or_else(|_| consts::GCP_METADATA_HOST_DEFAULT.to_string());
288+
289+
let metadata_url = format!(
290+
"http://{}{}?audience=prefix.dev",
291+
metadata_host,
292+
consts::GCP_METADATA_IDENTITY_PATH
293+
);
294+
let url = Url::parse(&metadata_url)?;
295+
296+
tracing::info!(
297+
"Querying the trusted publishing OIDC token from Google Cloud metadata server at {}",
298+
metadata_host
299+
);
300+
301+
let response = client
302+
.get(url.clone())
303+
.header("Metadata-Flavor", "Google")
304+
.send()
305+
.await
306+
.map_err(|err| TrustedPublishingError::ReqwestMiddleware(url.clone(), err))?;
307+
308+
if !response.status().is_success() {
309+
tracing::warn!(
310+
"Google Cloud metadata server returned status {}",
311+
response.status()
312+
);
313+
return Err(TrustedPublishingError::GoogleCloudOidcTokenNotFound);
314+
}
315+
316+
let token = response
317+
.text()
318+
.await
319+
.map_err(|err| TrustedPublishingError::Reqwest(url.clone(), err))?;
320+
321+
if token.is_empty() {
322+
tracing::warn!("Google Cloud metadata server returned empty token");
323+
return Err(TrustedPublishingError::GoogleCloudOidcTokenNotFound);
324+
}
325+
326+
tracing::info!("Successfully obtained OIDC token from Google Cloud");
327+
Ok(token)
328+
}
329+
165330
async fn get_publish_token(
166331
oidc_token: &str,
167332
prefix_url: &Url,

crates/rattler_upload/src/utils/console_utils.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,13 @@ use crate::utils::consts;
44
pub fn github_action_runner() -> bool {
55
std::env::var(consts::GITHUB_ACTIONS) == Ok("true".to_string())
66
}
7+
8+
/// Checks whether we are on GitLab CI
9+
pub fn gitlab_ci_runner() -> bool {
10+
std::env::var(consts::GITLAB_CI) == Ok("true".to_string())
11+
}
12+
13+
/// Checks whether we are on Google Cloud (Cloud Build, Cloud Run, GCE, etc.)
14+
pub fn google_cloud_runner() -> bool {
15+
std::env::var(consts::CLOUD_BUILD_ID).is_ok() || std::env::var(consts::K_SERVICE).is_ok()
16+
}

crates/rattler_upload/src/utils/consts.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,24 @@ pub const ACTIONS_ID_TOKEN_REQUEST_URL: &str = "ACTIONS_ID_TOKEN_REQUEST_URL";
77

88
/// This env var contains the oidc request token
99
pub const ACTIONS_ID_TOKEN_REQUEST_TOKEN: &str = "ACTIONS_ID_TOKEN_REQUEST_TOKEN";
10+
11+
// GitLab CI environment variables
12+
/// This env var is set to "true" when run inside a GitLab CI runner
13+
pub const GITLAB_CI: &str = "GITLAB_CI";
14+
15+
/// The default env var name for the GitLab OIDC ID token with audience "prefix.dev".
16+
/// Users should configure this in their `.gitlab-ci.yml` using the `id_tokens` keyword.
17+
pub const PREFIX_ID_TOKEN: &str = "PREFIX_ID_TOKEN";
18+
19+
// Google Cloud environment variables
20+
/// Set in Google Cloud Build
21+
pub const CLOUD_BUILD_ID: &str = "CLOUD_BUILD_ID";
22+
/// Set in Cloud Run
23+
pub const K_SERVICE: &str = "K_SERVICE";
24+
/// Environment variable to override the metadata server hostname (used by Google's libraries)
25+
pub const GCE_METADATA_HOST: &str = "GCE_METADATA_HOST";
26+
/// Default Google Cloud metadata server hostname
27+
pub const GCP_METADATA_HOST_DEFAULT: &str = "metadata.google.internal";
28+
/// Path to get identity tokens from the metadata server
29+
pub const GCP_METADATA_IDENTITY_PATH: &str =
30+
"/computeMetadata/v1/instance/service-accounts/default/identity";

0 commit comments

Comments
 (0)