Skip to main content

ambient_id/
buildkite.rs

1//! Buildkite OIDC token detection.
2
3use crate::DetectionStrategy;
4
5/// Possible errors during Buildkite OIDC token detection.
6#[derive(Debug, thiserror::Error)]
7pub enum Error {
8    /// An error occurred while executing the `buildkite-agent` command.
9    #[error("failed to obtain OIDC token from `buildkite-agent` CLI")]
10    Execution(#[from] std::io::Error),
11}
12
13pub(crate) struct Buildkite;
14
15impl DetectionStrategy for Buildkite {
16    type Error = Error;
17
18    fn new(_state: &crate::DetectionState) -> Option<Self>
19    where
20        Self: Sized,
21    {
22        // https://buildkite.com/docs/pipelines/configure/environment-variables#buildkite-environment-variables
23        std::env::var("BUILDKITE")
24            .ok()
25            .filter(|v| v == "true")
26            .map(|_| Buildkite)
27    }
28
29    /// On Buildkite, the OIDC token is provided by the `buildkite-agent`
30    /// tool. Specifically, we need to invoke:
31    ///
32    /// ```sh
33    /// buildkite-agent oidc request-token --audience <audience>
34    /// ```
35    ///
36    /// The standard output of this command is the ID token on success.
37    async fn detect(&self, audience: &str) -> Result<crate::IdToken, Self::Error> {
38        let output = std::process::Command::new("buildkite-agent")
39            .args(&["oidc", "request-token", "--audience", audience])
40            .output()?;
41
42        if !output.status.success() {
43            return Err(Error::Execution(std::io::Error::new(
44                std::io::ErrorKind::Other,
45                format!(
46                    "`buildkite-agent` exited with code {status}: '{stderr}'",
47                    status = output.status,
48                    stderr = String::from_utf8_lossy(&output.stderr),
49                ),
50            )));
51        }
52
53        let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
54        Ok(crate::IdToken(token.into()))
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use crate::{DetectionStrategy as _, buildkite::Buildkite, tests::EnvScope};
61
62    #[tokio::test]
63    async fn test_not_detected() {
64        let mut scope = EnvScope::new();
65        scope.unsetenv("BUILDKITE");
66
67        let state = Default::default();
68        assert!(Buildkite::new(&state).is_none());
69    }
70
71    #[tokio::test]
72    async fn test_detected() {
73        let mut scope = EnvScope::new();
74        scope.setenv("BUILDKITE", "true");
75
76        let state = Default::default();
77        assert!(Buildkite::new(&state).is_some());
78    }
79
80    /// Happy path for Buildkite OIDC token detection.
81    #[tokio::test]
82    #[cfg_attr(not(feature = "test-buildkite-1p"), ignore)]
83    async fn test_1p_detection_ok() {
84        let _ = EnvScope::new();
85        let state = Default::default();
86        let detector = Buildkite::new(&state).expect("should detect Buildkite");
87        let token = detector
88            .detect("test_1p_detection_ok")
89            .await
90            .expect("should fetch token");
91
92        assert!(token.reveal().starts_with("eyJ"));
93    }
94}