Skip to main content

blue_build_utils/
credentials.rs

1use std::{
2    collections::HashMap,
3    sync::{LazyLock, Mutex},
4};
5
6use bon::Builder;
7use clap::Args;
8use docker_credential::DockerCredential;
9use log::trace;
10
11use crate::{
12    constants::{
13        BB_PASSWORD, BB_REGISTRY, BB_USERNAME, CI_REGISTRY, CI_REGISTRY_PASSWORD, CI_REGISTRY_USER,
14        GITHUB_ACTIONS, GITHUB_ACTOR, GITHUB_TOKEN,
15    },
16    get_env_var,
17    secret::SecretValue,
18    string,
19};
20
21static CREDS: LazyLock<Mutex<HashMap<String, Credentials>>> =
22    LazyLock::new(|| Mutex::new(HashMap::default()));
23
24/// The credentials for logging into image registries.
25#[derive(Debug, Clone)]
26pub enum Credentials {
27    Basic {
28        username: String,
29        password: SecretValue,
30    },
31    Token(SecretValue),
32}
33
34impl Credentials {
35    /// Set the users credentials for
36    /// the current set of actions.
37    ///
38    /// Be sure to call this before trying to use
39    /// any strategy that requires credentials as
40    /// the environment credentials are lazy allocated.
41    ///
42    /// # Panics
43    /// Will panic if it can't lock the mutex.
44    pub fn init(args: CredentialsArgs) {
45        trace!("Credentials::init()");
46        let mut creds = CREDS.lock().expect("Must lock CREDS");
47
48        let CredentialsArgs {
49            username,
50            password,
51            registry,
52        } = args;
53
54        let registry = match (
55            registry,
56            get_env_var(CI_REGISTRY).ok(),
57            get_env_var(GITHUB_ACTIONS).ok(),
58        ) {
59            (Some(registry), _, _) | (_, Some(registry), _) if !registry.is_empty() => registry,
60            (_, _, Some(_)) => string!("ghcr.io"),
61            _ => return,
62        };
63
64        let cred = match (
65            (username, password),
66            docker_credential::get_credential(&registry).ok(),
67            docker_credential::get_podman_credential(&registry).ok(),
68            (
69                get_env_var(CI_REGISTRY_USER).ok(),
70                get_env_var(CI_REGISTRY_PASSWORD)
71                    .ok()
72                    .map(SecretValue::from),
73            ),
74            (
75                get_env_var(GITHUB_ACTOR).ok(),
76                get_env_var(GITHUB_TOKEN).ok().map(SecretValue::from),
77            ),
78        ) {
79            ((Some(username), Some(password)), _, _, _, _) => Self::Basic { username, password },
80            (_, Some(DockerCredential::UsernamePassword(username, password)), _, _, _)
81            | (_, _, Some(DockerCredential::UsernamePassword(username, password)), _, _) => {
82                Self::Basic {
83                    username,
84                    password: password.into(),
85                }
86            }
87            (_, Some(DockerCredential::IdentityToken(token)), _, _, _)
88            | (_, _, Some(DockerCredential::IdentityToken(token)), _, _) => {
89                Self::Token(token.into())
90            }
91            (_, _, _, (Some(username), Some(password)), _)
92            | (_, _, _, _, (Some(username), Some(password)))
93                if !username.is_empty() && !password.is_empty() =>
94            {
95                Self::Basic { username, password }
96            }
97            _ => return,
98        };
99
100        let _ = creds.insert(registry, cred);
101        drop(creds);
102    }
103
104    /// Get the credentials for the current set of actions.
105    ///
106    /// # Panics
107    /// Will panic if it can't lock the mutex.
108    #[must_use]
109    pub fn get(registry: &str) -> Option<Self> {
110        trace!("credentials::get({registry})");
111
112        let mut creds = CREDS.lock().expect("Must lock CREDS");
113
114        match (
115            creds.get(registry),
116            docker_credential::get_credential(registry).ok(),
117            docker_credential::get_podman_credential(registry).ok(),
118        ) {
119            (Some(creds), _, _) => Some(creds.clone()),
120            (None, Some(DockerCredential::IdentityToken(token)), _)
121            | (None, None, Some(DockerCredential::IdentityToken(token))) => {
122                let cred = Self::Token(SecretValue::from(token));
123                let _ = creds.insert(registry.into(), cred.clone());
124                drop(creds);
125                Some(cred)
126            }
127            (None, Some(DockerCredential::UsernamePassword(username, password)), _)
128            | (None, None, Some(DockerCredential::UsernamePassword(username, password))) => {
129                let cred = Self::Basic {
130                    username,
131                    password: password.into(),
132                };
133                let _ = creds.insert(registry.into(), cred.clone());
134                drop(creds);
135                Some(cred)
136            }
137            (None, None, None) => None,
138        }
139    }
140}
141
142#[derive(Debug, Default, Clone, Builder, Args)]
143#[builder(on(String, into))]
144pub struct CredentialsArgs {
145    /// The registry's domain name.
146    #[arg(long, env = BB_REGISTRY)]
147    pub registry: Option<String>,
148
149    /// The username to login to the
150    /// container registry.
151    #[arg(short = 'U', long, env = BB_USERNAME, hide_env_values = true)]
152    pub username: Option<String>,
153
154    /// The password to login to the
155    /// container registry.
156    #[arg(short = 'P', long, env = BB_PASSWORD, hide_env_values = true)]
157    pub password: Option<SecretValue>,
158}