libcnb_test/
pack.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3use std::process::Command;
4
5/// Represents a `pack build` command.
6#[derive(Clone, Debug)]
7pub(crate) struct PackBuildCommand {
8    build_cache_volume_name: String,
9    builder: String,
10    buildpacks: Vec<BuildpackReference>,
11    env: BTreeMap<String, String>,
12    image_name: String,
13    launch_cache_volume_name: String,
14    path: PathBuf,
15    pull_policy: PullPolicy,
16    trust_builder: bool,
17    trust_extra_buildpacks: bool,
18}
19
20#[derive(Clone, Debug)]
21pub(crate) enum BuildpackReference {
22    Id(String),
23    Path(PathBuf),
24}
25
26impl From<PathBuf> for BuildpackReference {
27    fn from(path: PathBuf) -> Self {
28        Self::Path(path)
29    }
30}
31
32impl From<String> for BuildpackReference {
33    fn from(id: String) -> Self {
34        Self::Id(id)
35    }
36}
37
38#[derive(Clone, Debug)]
39/// Controls whether Pack should pull images.
40#[allow(dead_code)]
41pub(crate) enum PullPolicy {
42    /// Always pull images.
43    Always,
44    /// Use local images if they are already present, rather than pulling updated images.
45    IfNotPresent,
46    /// Never pull images. If the required images are not already available locally the pack command will fail.
47    Never,
48}
49
50impl PackBuildCommand {
51    pub(crate) fn new(
52        builder: impl Into<String>,
53        path: impl Into<PathBuf>,
54        image_name: impl Into<String>,
55        build_cache_volume_name: impl Into<String>,
56        launch_cache_volume_name: impl Into<String>,
57    ) -> Self {
58        Self {
59            build_cache_volume_name: build_cache_volume_name.into(),
60            builder: builder.into(),
61            buildpacks: Vec::new(),
62            env: BTreeMap::new(),
63            image_name: image_name.into(),
64            launch_cache_volume_name: launch_cache_volume_name.into(),
65            path: path.into(),
66            // Prevent redundant image-pulling, which slows tests and risks hitting registry rate limits.
67            pull_policy: PullPolicy::IfNotPresent,
68            trust_builder: true,
69            trust_extra_buildpacks: true,
70        }
71    }
72
73    pub(crate) fn buildpack(&mut self, b: impl Into<BuildpackReference>) -> &mut Self {
74        self.buildpacks.push(b.into());
75        self
76    }
77
78    pub(crate) fn env(&mut self, k: impl Into<String>, v: impl Into<String>) -> &mut Self {
79        self.env.insert(k.into(), v.into());
80        self
81    }
82}
83
84impl From<PackBuildCommand> for Command {
85    fn from(pack_build_command: PackBuildCommand) -> Self {
86        let mut command = Self::new("pack");
87
88        command.args([
89            "build",
90            &pack_build_command.image_name,
91            "--builder",
92            &pack_build_command.builder,
93            "--cache",
94            &format!(
95                "type=build;format=volume;name={}",
96                pack_build_command.build_cache_volume_name
97            ),
98            "--cache",
99            &format!(
100                "type=launch;format=volume;name={}",
101                pack_build_command.launch_cache_volume_name
102            ),
103            "--path",
104            &pack_build_command.path.to_string_lossy(),
105            "--pull-policy",
106            match pack_build_command.pull_policy {
107                PullPolicy::Always => "always",
108                PullPolicy::IfNotPresent => "if-not-present",
109                PullPolicy::Never => "never",
110            },
111        ]);
112
113        for buildpack in pack_build_command.buildpacks {
114            command.args([
115                "--buildpack",
116                &match buildpack {
117                    BuildpackReference::Id(id) => id,
118                    BuildpackReference::Path(path_buf) => path_buf.to_string_lossy().to_string(),
119                },
120            ]);
121        }
122
123        for (env_key, env_value) in &pack_build_command.env {
124            command.args(["--env", &format!("{env_key}={env_value}")]);
125        }
126
127        if pack_build_command.trust_builder {
128            command.arg("--trust-builder");
129        }
130
131        if pack_build_command.trust_extra_buildpacks {
132            command.arg("--trust-extra-buildpacks");
133        }
134
135        command
136    }
137}
138
139#[derive(Clone, Debug)]
140pub(crate) struct PackSbomDownloadCommand {
141    image_name: String,
142    output_dir: Option<PathBuf>,
143}
144
145/// Represents a `pack sbom download` command.
146impl PackSbomDownloadCommand {
147    pub(crate) fn new(image_name: impl Into<String>) -> Self {
148        Self {
149            image_name: image_name.into(),
150            output_dir: None,
151        }
152    }
153
154    pub(crate) fn output_dir(&mut self, output_dir: impl Into<PathBuf>) -> &mut Self {
155        self.output_dir = Some(output_dir.into());
156        self
157    }
158}
159
160impl From<PackSbomDownloadCommand> for Command {
161    fn from(pack_command: PackSbomDownloadCommand) -> Self {
162        let mut command = Self::new("pack");
163
164        command.args(["sbom", "download", &pack_command.image_name]);
165
166        if let Some(output_dir) = pack_command.output_dir {
167            command.args(["--output-dir", &output_dir.to_string_lossy()]);
168        }
169
170        command
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use std::ffi::OsStr;
178
179    #[test]
180    fn from_pack_build_command_to_command() {
181        let mut input = PackBuildCommand {
182            build_cache_volume_name: String::from("build-cache-volume"),
183            builder: String::from("builder:20"),
184            buildpacks: vec![
185                BuildpackReference::Id(String::from("libcnb/buildpack1")),
186                BuildpackReference::Path(PathBuf::from("/tmp/buildpack2")),
187            ],
188            env: BTreeMap::from([
189                (String::from("ENV_FOO"), String::from("FOO_VALUE")),
190                (String::from("ENV_BAR"), String::from("WHITESPACE VALUE")),
191            ]),
192            image_name: String::from("my-image"),
193            launch_cache_volume_name: String::from("launch-cache-volume"),
194            path: PathBuf::from("/tmp/foo/bar"),
195            pull_policy: PullPolicy::IfNotPresent,
196            trust_builder: true,
197            trust_extra_buildpacks: true,
198        };
199
200        let command: Command = input.clone().into();
201
202        assert_eq!(command.get_program(), "pack");
203
204        assert_eq!(
205            command.get_args().collect::<Vec<&OsStr>>(),
206            [
207                "build",
208                "my-image",
209                "--builder",
210                "builder:20",
211                "--cache",
212                "type=build;format=volume;name=build-cache-volume",
213                "--cache",
214                "type=launch;format=volume;name=launch-cache-volume",
215                "--path",
216                "/tmp/foo/bar",
217                "--pull-policy",
218                "if-not-present",
219                "--buildpack",
220                "libcnb/buildpack1",
221                "--buildpack",
222                "/tmp/buildpack2",
223                "--env",
224                "ENV_BAR=WHITESPACE VALUE",
225                "--env",
226                "ENV_FOO=FOO_VALUE",
227                "--trust-builder",
228                "--trust-extra-buildpacks",
229            ]
230        );
231
232        assert_eq!(command.get_envs().collect::<Vec<_>>(), Vec::new());
233
234        // Assert conditional '--trust-builder' flag works as expected:
235        input.trust_builder = false;
236        let command: Command = input.clone().into();
237        assert!(
238            !command
239                .get_args()
240                .any(|arg| arg == OsStr::new("--trust-builder"))
241        );
242    }
243
244    #[test]
245    fn from_pack_sbom_download_command_to_command() {
246        let mut input = PackSbomDownloadCommand {
247            image_name: String::from("my-image"),
248            output_dir: None,
249        };
250
251        let command: Command = input.clone().into();
252
253        assert_eq!(command.get_program(), "pack");
254
255        assert_eq!(
256            command.get_args().collect::<Vec<&OsStr>>(),
257            ["sbom", "download", "my-image"]
258        );
259
260        assert_eq!(command.get_envs().collect::<Vec<_>>(), Vec::new());
261
262        // Assert conditional '--output-dir' flag works as expected:
263        input.output_dir = Some(PathBuf::from("/tmp/sboms"));
264        let command: Command = input.into();
265
266        assert_eq!(command.get_program(), "pack");
267
268        assert_eq!(
269            command.get_args().collect::<Vec<&OsStr>>(),
270            ["sbom", "download", "my-image", "--output-dir", "/tmp/sboms"]
271        );
272
273        assert_eq!(command.get_envs().collect::<Vec<_>>(), Vec::new());
274    }
275}