Skip to main content

blue_build_process_management/drivers/
gitlab_driver.rs

1use std::path::PathBuf;
2
3use blue_build_utils::{
4    constants::{
5        CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID,
6        CI_PIPELINE_SOURCE, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_PROJECT_URL, CI_REGISTRY,
7        CI_SERVER_HOST, CI_SERVER_PROTOCOL,
8    },
9    container::Tag,
10    string_vec,
11};
12use log::trace;
13
14#[cfg(not(test))]
15use blue_build_utils::get_env_var;
16
17#[cfg(test)]
18use blue_build_utils::test_utils::get_env_var;
19use miette::Result;
20
21use crate::drivers::Driver;
22
23use super::{CiDriver, opts::GenerateTagsOpts};
24
25pub struct GitlabDriver;
26
27impl CiDriver for GitlabDriver {
28    fn on_default_branch() -> bool {
29        get_env_var(CI_DEFAULT_BRANCH).is_ok_and(|default_branch| {
30            get_env_var(CI_COMMIT_REF_NAME).is_ok_and(|branch| default_branch == branch)
31        })
32    }
33
34    fn keyless_cert_identity() -> Result<String> {
35        Ok(format!(
36            "{}//.gitlab-ci.yml@refs/heads/{}",
37            get_env_var(CI_PROJECT_URL)?,
38            get_env_var(CI_DEFAULT_BRANCH)?,
39        ))
40    }
41
42    fn oidc_provider() -> miette::Result<String> {
43        Ok(format!(
44            "{}://{}",
45            get_env_var(CI_SERVER_PROTOCOL)?,
46            get_env_var(CI_SERVER_HOST)?,
47        ))
48    }
49
50    fn generate_tags(opts: GenerateTagsOpts) -> Result<Vec<Tag>> {
51        const MR_EVENT: &str = "merge_request_event";
52        let os_version = Driver::get_os_version().oci_ref(opts.oci_ref).call()?;
53        let timestamp = blue_build_utils::get_tag_timestamp();
54        let short_sha =
55            get_env_var(CI_COMMIT_SHORT_SHA).inspect(|v| trace!("{CI_COMMIT_SHORT_SHA}={v}"))?;
56        let ref_name = get_env_var(CI_COMMIT_REF_NAME)
57            .inspect(|v| trace!("{CI_COMMIT_REF_NAME}={v}"))?
58            .replace('/', "_");
59
60        let tags = match (
61            Self::on_default_branch(),
62            opts.alt_tags.as_ref(),
63            get_env_var(CI_MERGE_REQUEST_IID).inspect(|v| trace!("{CI_MERGE_REQUEST_IID}={v}")),
64            get_env_var(CI_PIPELINE_SOURCE).inspect(|v| trace!("{CI_PIPELINE_SOURCE}={v}")),
65        ) {
66            (true, None, _, _) => {
67                string_vec![
68                    "latest",
69                    &timestamp,
70                    format!("{os_version}"),
71                    format!("{timestamp}-{os_version}"),
72                    format!("{short_sha}-{os_version}"),
73                ]
74            }
75            (true, Some(alt_tags), _, _) => alt_tags
76                .iter()
77                .flat_map(|alt| {
78                    string_vec![
79                        alt,
80                        format!("{alt}-{os_version}"),
81                        format!("{timestamp}-{alt}-{os_version}"),
82                        format!("{short_sha}-{alt}-{os_version}"),
83                    ]
84                })
85                .collect(),
86            (false, None, Ok(mr_iid), Ok(pipeline_source)) if pipeline_source == MR_EVENT => {
87                vec![
88                    format!("mr-{mr_iid}-{os_version}"),
89                    format!("{short_sha}-{os_version}"),
90                ]
91            }
92            (false, None, _, _) => {
93                vec![
94                    format!("br-{ref_name}-{os_version}"),
95                    format!("{short_sha}-{os_version}"),
96                ]
97            }
98            (false, Some(alt_tags), Ok(mr_iid), Ok(pipeline_source))
99                if pipeline_source == MR_EVENT =>
100            {
101                alt_tags
102                    .iter()
103                    .flat_map(|alt| {
104                        vec![
105                            format!("mr-{mr_iid}-{alt}-{os_version}"),
106                            format!("{short_sha}-{alt}-{os_version}"),
107                        ]
108                    })
109                    .collect()
110            }
111            (false, Some(alt_tags), _, _) => alt_tags
112                .iter()
113                .flat_map(|alt| {
114                    vec![
115                        format!("br-{ref_name}-{alt}-{os_version}"),
116                        format!("{short_sha}-{alt}-{os_version}"),
117                    ]
118                })
119                .collect(),
120        }
121        .into_iter()
122        .map(|tag| tag.parse())
123        .collect::<Result<Vec<Tag>>>()?;
124        trace!("{tags:?}");
125
126        Ok(tags)
127    }
128
129    fn get_repo_url() -> miette::Result<String> {
130        Ok(format!(
131            "{}://{}/{}/{}",
132            get_env_var(CI_SERVER_PROTOCOL)?,
133            get_env_var(CI_SERVER_HOST)?,
134            get_env_var(CI_PROJECT_NAMESPACE)?,
135            get_env_var(CI_PROJECT_NAME)?,
136        ))
137    }
138
139    fn get_registry() -> miette::Result<String> {
140        Ok(format!(
141            "{}/{}/{}",
142            get_env_var(CI_REGISTRY)?,
143            get_env_var(CI_PROJECT_NAMESPACE)?,
144            get_env_var(CI_PROJECT_NAME)?,
145        )
146        .to_lowercase())
147    }
148
149    fn default_ci_file_path() -> PathBuf {
150        PathBuf::from(".gitlab-ci.yml")
151    }
152}
153
154#[cfg(test)]
155mod test {
156    use blue_build_utils::{
157        constants::{
158            CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID,
159            CI_PIPELINE_SOURCE, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_REGISTRY, CI_SERVER_HOST,
160            CI_SERVER_PROTOCOL,
161        },
162        container::Tag,
163        platform::Platform,
164        string_vec,
165        test_utils::set_env_var,
166    };
167    use oci_client::Reference;
168    use rstest::rstest;
169
170    use crate::{
171        drivers::{CiDriver, opts::GenerateTagsOpts},
172        test::{TEST_TAG_1, TEST_TAG_2, TIMESTAMP},
173    };
174
175    use super::GitlabDriver;
176
177    const COMMIT_SHA: &str = "1234567";
178    const BR_REF_NAME: &str = "feature/test";
179    const BR_REF_NAME_CLEAN: &str = "feature_test";
180
181    fn setup_default_branch() {
182        setup();
183        set_env_var(CI_COMMIT_REF_NAME, "main");
184    }
185
186    fn setup_mr_branch() {
187        setup();
188        set_env_var(CI_MERGE_REQUEST_IID, "12");
189        set_env_var(CI_PIPELINE_SOURCE, "merge_request_event");
190        set_env_var(CI_COMMIT_REF_NAME, BR_REF_NAME);
191    }
192
193    fn setup_branch() {
194        setup();
195        set_env_var(CI_COMMIT_REF_NAME, BR_REF_NAME);
196    }
197
198    fn setup() {
199        set_env_var(CI_DEFAULT_BRANCH, "main");
200        set_env_var(CI_COMMIT_SHORT_SHA, COMMIT_SHA);
201        set_env_var(CI_REGISTRY, "registry.example.com");
202        set_env_var(CI_PROJECT_NAMESPACE, "test-project");
203        set_env_var(CI_PROJECT_NAME, "test");
204        set_env_var(CI_SERVER_PROTOCOL, "https");
205        set_env_var(CI_SERVER_HOST, "gitlab.example.com");
206    }
207
208    #[test]
209    fn get_registry() {
210        setup();
211
212        let registry = GitlabDriver::get_registry().unwrap();
213
214        assert_eq!(registry, "registry.example.com/test-project/test");
215    }
216
217    #[test]
218    fn on_default_branch_true() {
219        setup_default_branch();
220
221        assert!(GitlabDriver::on_default_branch());
222    }
223
224    #[test]
225    fn on_default_branch_false() {
226        setup_branch();
227
228        assert!(!GitlabDriver::on_default_branch());
229    }
230
231    #[test]
232    fn get_repo_url() {
233        setup();
234
235        let url = GitlabDriver::get_repo_url().unwrap();
236
237        assert_eq!(url, "https://gitlab.example.com/test-project/test");
238    }
239
240    #[rstest]
241    #[case::default_branch(
242        setup_default_branch,
243        None,
244        string_vec![
245            format!("{}-41", &*TIMESTAMP),
246            "latest",
247            &*TIMESTAMP,
248            format!("{COMMIT_SHA}-41"),
249            "41",
250        ],
251    )]
252    #[case::default_branch_alt_tags(
253        setup_default_branch,
254        Some(bon::vec![TEST_TAG_1, TEST_TAG_2]),
255        string_vec![
256            TEST_TAG_1,
257            format!("{TEST_TAG_1}-41"),
258            format!("{}-{TEST_TAG_1}-41", &*TIMESTAMP),
259            format!("{COMMIT_SHA}-{TEST_TAG_1}-41"),
260            TEST_TAG_2,
261            format!("{TEST_TAG_2}-41"),
262            format!("{}-{TEST_TAG_2}-41", &*TIMESTAMP),
263            format!("{COMMIT_SHA}-{TEST_TAG_2}-41"),
264        ],
265    )]
266    #[case::pr_branch(
267        setup_mr_branch,
268        None,
269        string_vec!["mr-12-41", format!("{COMMIT_SHA}-41")],
270    )]
271    #[case::pr_branch_alt_tags(
272        setup_mr_branch,
273        Some(bon::vec![TEST_TAG_1, TEST_TAG_2]),
274        string_vec![
275            format!("mr-12-{TEST_TAG_1}-41"),
276            format!("{COMMIT_SHA}-{TEST_TAG_1}-41"),
277            format!("mr-12-{TEST_TAG_2}-41"),
278            format!("{COMMIT_SHA}-{TEST_TAG_2}-41"),
279        ],
280    )]
281    #[case::branch(
282        setup_branch,
283        None,
284        string_vec![format!("{COMMIT_SHA}-41"), format!("br-{BR_REF_NAME_CLEAN}-41")],
285    )]
286    #[case::branch_alt_tags(
287        setup_branch,
288        Some(bon::vec![TEST_TAG_1, TEST_TAG_2]),
289        string_vec![
290            format!("br-{BR_REF_NAME_CLEAN}-{TEST_TAG_1}-41"),
291            format!("{COMMIT_SHA}-{TEST_TAG_1}-41"),
292            format!("br-{BR_REF_NAME_CLEAN}-{TEST_TAG_2}-41"),
293            format!("{COMMIT_SHA}-{TEST_TAG_2}-41"),
294        ],
295    )]
296    fn generate_tags(
297        #[case] setup: impl FnOnce(),
298        #[case] alt_tags: Option<Vec<String>>,
299        #[case] mut expected: Vec<String>,
300    ) {
301        setup();
302        expected.sort();
303        let expected: Vec<Tag> = expected
304            .into_iter()
305            .map(|tag| tag.parse().unwrap())
306            .collect();
307        let oci_ref: Reference = "ghcr.io/ublue-os/silverblue-main".parse().unwrap();
308        let alt_tags = alt_tags.map(|tags| {
309            tags.into_iter()
310                .map(|tag| tag.parse::<Tag>().unwrap())
311                .collect::<Vec<_>>()
312        });
313
314        let mut tags = GitlabDriver::generate_tags(
315            GenerateTagsOpts::builder()
316                .oci_ref(&oci_ref)
317                .maybe_alt_tags(alt_tags.as_deref())
318                .platform(Platform::LinuxAmd64)
319                .build(),
320        )
321        .unwrap();
322        tags.sort();
323
324        assert_eq!(tags, expected);
325    }
326}