docker_wrapper/command/
bake.rs

1//! Docker Bake Command Implementation
2//!
3//! This module provides a comprehensive implementation of the `docker bake` command,
4//! supporting all native Docker buildx bake options for building from configuration files.
5//!
6//! # Examples
7//!
8//! ## Basic Usage
9//!
10//! ```no_run
11//! use docker_wrapper::BakeCommand;
12//! use docker_wrapper::DockerCommand;
13//!
14//! #[tokio::main]
15//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
16//!     // Basic bake with default docker-bake.hcl file
17//!     let bake_cmd = BakeCommand::new();
18//!     let output = bake_cmd.execute().await?;
19//!     println!("Bake completed: {}", output.success);
20//!     Ok(())
21//! }
22//! ```
23//!
24//! ## Advanced Usage
25//!
26//! ```no_run
27//! use docker_wrapper::BakeCommand;
28//! use docker_wrapper::DockerCommand;
29//!
30//! #[tokio::main]
31//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
32//!     // Advanced bake with custom file and targets
33//!     let bake_cmd = BakeCommand::new()
34//!         .file("docker-compose.yml")
35//!         .file("custom-bake.hcl")
36//!         .target("web")
37//!         .target("api")
38//!         .push()
39//!         .no_cache()
40//!         .set("web.platform", "linux/amd64,linux/arm64")
41//!         .metadata_file("build-metadata.json");
42//!
43//!     let output = bake_cmd.execute().await?;
44//!     println!("Multi-target bake completed: {}", output.success);
45//!     Ok(())
46//! }
47//! ```
48
49use super::{CommandExecutor, CommandOutput, DockerCommand};
50use crate::error::Result;
51use async_trait::async_trait;
52use std::collections::HashMap;
53
54/// Docker Bake Command Builder
55///
56/// Implements the `docker bake` command for building from configuration files
57/// like docker-compose.yml, docker-bake.hcl, or custom bake definitions.
58///
59/// # Docker Bake Overview
60///
61/// The bake command allows you to build multiple targets defined in configuration
62/// files, supporting advanced features like:
63/// - Multi-platform builds
64/// - Build matrix configurations
65/// - Shared build contexts
66/// - Variable substitution
67/// - Target dependencies
68///
69/// # Supported File Formats
70///
71/// - `docker-compose.yml` - Docker Compose service definitions
72/// - `docker-bake.hcl` - HCL (`HashiCorp` Configuration Language) format
73/// - `docker-bake.json` - JSON format
74/// - Custom build definition files
75///
76/// # Examples
77///
78/// ```no_run
79/// use docker_wrapper::BakeCommand;
80/// use docker_wrapper::DockerCommand;
81///
82/// #[tokio::main]
83/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
84///     // Build all targets from docker-compose.yml
85///     let output = BakeCommand::new()
86///         .file("docker-compose.yml")
87///         .execute()
88///         .await?;
89///
90///     println!("Bake success: {}", output.success);
91///     Ok(())
92/// }
93/// ```
94#[derive(Debug, Clone)]
95#[allow(clippy::struct_excessive_bools)]
96pub struct BakeCommand {
97    /// Build targets to build (defaults to all targets if empty)
98    targets: Vec<String>,
99    /// Build definition files
100    files: Vec<String>,
101    /// Resource access permissions
102    allow: Vec<String>,
103    /// Builder instance override
104    builder: Option<String>,
105    /// Evaluation method (build, check, outline, targets)
106    call: Option<String>,
107    /// Enable check mode (shorthand for --call=check)
108    check: bool,
109    /// Enable debug logging
110    debug: bool,
111    /// List targets or variables
112    list: Option<String>,
113    /// Load images to Docker daemon (shorthand for --set=*.output=type=docker)
114    load: bool,
115    /// Build result metadata file
116    metadata_file: Option<String>,
117    /// Disable build cache
118    no_cache: bool,
119    /// Print options without building
120    print: bool,
121    /// Progress output type
122    progress: Option<String>,
123    /// Provenance attestation (shorthand for --set=*.attest=type=provenance)
124    provenance: Option<String>,
125    /// Always pull referenced images
126    pull: bool,
127    /// Push images to registry (shorthand for --set=*.output=type=registry)
128    push: bool,
129    /// SBOM attestation (shorthand for --set=*.attest=type=sbom)
130    sbom: Option<String>,
131    /// Target value overrides (key=value pairs)
132    set_values: HashMap<String, String>,
133    /// Command executor for handling raw arguments and execution
134    pub executor: CommandExecutor,
135}
136
137impl BakeCommand {
138    /// Create a new `BakeCommand` instance
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use docker_wrapper::BakeCommand;
144    ///
145    /// let bake_cmd = BakeCommand::new();
146    /// ```
147    #[must_use]
148    pub fn new() -> Self {
149        Self {
150            targets: Vec::new(),
151            files: Vec::new(),
152            allow: Vec::new(),
153            builder: None,
154            call: None,
155            check: false,
156            debug: false,
157            list: None,
158            load: false,
159            metadata_file: None,
160            no_cache: false,
161            print: false,
162            progress: None,
163            provenance: None,
164            pull: false,
165            push: false,
166            sbom: None,
167            set_values: HashMap::new(),
168            executor: CommandExecutor::new(),
169        }
170    }
171
172    /// Add a target to build
173    ///
174    /// Multiple targets can be specified. If no targets are specified,
175    /// all targets defined in the bake file will be built.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use docker_wrapper::BakeCommand;
181    ///
182    /// let bake_cmd = BakeCommand::new()
183    ///     .target("web")
184    ///     .target("api")
185    ///     .target("worker");
186    /// ```
187    #[must_use]
188    pub fn target<S: Into<String>>(mut self, target: S) -> Self {
189        self.targets.push(target.into());
190        self
191    }
192
193    /// Add multiple targets to build
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use docker_wrapper::BakeCommand;
199    ///
200    /// let bake_cmd = BakeCommand::new()
201    ///     .targets(vec!["web", "api", "worker"]);
202    /// ```
203    #[must_use]
204    pub fn targets<I, S>(mut self, targets: I) -> Self
205    where
206        I: IntoIterator<Item = S>,
207        S: Into<String>,
208    {
209        self.targets
210            .extend(targets.into_iter().map(std::convert::Into::into));
211        self
212    }
213
214    /// Add a build definition file
215    ///
216    /// Supports docker-compose.yml, docker-bake.hcl, docker-bake.json,
217    /// and custom build definition files.
218    ///
219    /// # Examples
220    ///
221    /// ```
222    /// use docker_wrapper::BakeCommand;
223    ///
224    /// let bake_cmd = BakeCommand::new()
225    ///     .file("docker-compose.yml")
226    ///     .file("custom-bake.hcl");
227    /// ```
228    #[must_use]
229    pub fn file<S: Into<String>>(mut self, file: S) -> Self {
230        self.files.push(file.into());
231        self
232    }
233
234    /// Add multiple build definition files
235    ///
236    /// # Examples
237    ///
238    /// ```
239    /// use docker_wrapper::BakeCommand;
240    ///
241    /// let bake_cmd = BakeCommand::new()
242    ///     .files(vec!["docker-compose.yml", "override.yml"]);
243    /// ```
244    #[must_use]
245    pub fn files<I, S>(mut self, files: I) -> Self
246    where
247        I: IntoIterator<Item = S>,
248        S: Into<String>,
249    {
250        self.files
251            .extend(files.into_iter().map(std::convert::Into::into));
252        self
253    }
254
255    /// Allow build to access specified resources
256    ///
257    /// Grants permission to access host resources during build.
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use docker_wrapper::BakeCommand;
263    ///
264    /// let bake_cmd = BakeCommand::new()
265    ///     .allow("network.host")
266    ///     .allow("security.insecure");
267    /// ```
268    #[must_use]
269    pub fn allow<S: Into<String>>(mut self, resource: S) -> Self {
270        self.allow.push(resource.into());
271        self
272    }
273
274    /// Override the configured builder instance
275    ///
276    /// # Examples
277    ///
278    /// ```
279    /// use docker_wrapper::BakeCommand;
280    ///
281    /// let bake_cmd = BakeCommand::new()
282    ///     .builder("mybuilder");
283    /// ```
284    #[must_use]
285    pub fn builder<S: Into<String>>(mut self, builder: S) -> Self {
286        self.builder = Some(builder.into());
287        self
288    }
289
290    /// Set method for evaluating build
291    ///
292    /// Valid values: "build", "check", "outline", "targets"
293    ///
294    /// # Examples
295    ///
296    /// ```
297    /// use docker_wrapper::BakeCommand;
298    ///
299    /// let bake_cmd = BakeCommand::new()
300    ///     .call("check"); // Validate build configuration
301    /// ```
302    #[must_use]
303    pub fn call<S: Into<String>>(mut self, method: S) -> Self {
304        self.call = Some(method.into());
305        self
306    }
307
308    /// Enable check mode (shorthand for --call=check)
309    ///
310    /// Validates the build configuration without executing the build.
311    ///
312    /// # Examples
313    ///
314    /// ```
315    /// use docker_wrapper::BakeCommand;
316    ///
317    /// let bake_cmd = BakeCommand::new()
318    ///     .check();
319    /// ```
320    #[must_use]
321    pub fn check(mut self) -> Self {
322        self.check = true;
323        self
324    }
325
326    /// Enable debug logging
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use docker_wrapper::BakeCommand;
332    ///
333    /// let bake_cmd = BakeCommand::new()
334    ///     .debug();
335    /// ```
336    #[must_use]
337    pub fn debug(mut self) -> Self {
338        self.debug = true;
339        self
340    }
341
342    /// List targets or variables
343    ///
344    /// # Examples
345    ///
346    /// ```
347    /// use docker_wrapper::BakeCommand;
348    ///
349    /// let bake_cmd = BakeCommand::new()
350    ///     .list("targets"); // List all available targets
351    /// ```
352    #[must_use]
353    pub fn list<S: Into<String>>(mut self, list_type: S) -> Self {
354        self.list = Some(list_type.into());
355        self
356    }
357
358    /// Load images to Docker daemon (shorthand for --set=*.output=type=docker)
359    ///
360    /// # Examples
361    ///
362    /// ```
363    /// use docker_wrapper::BakeCommand;
364    ///
365    /// let bake_cmd = BakeCommand::new()
366    ///     .load();
367    /// ```
368    #[must_use]
369    pub fn load(mut self) -> Self {
370        self.load = true;
371        self
372    }
373
374    /// Write build result metadata to a file
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// use docker_wrapper::BakeCommand;
380    ///
381    /// let bake_cmd = BakeCommand::new()
382    ///     .metadata_file("build-metadata.json");
383    /// ```
384    #[must_use]
385    pub fn metadata_file<S: Into<String>>(mut self, file: S) -> Self {
386        self.metadata_file = Some(file.into());
387        self
388    }
389
390    /// Do not use cache when building images
391    ///
392    /// # Examples
393    ///
394    /// ```
395    /// use docker_wrapper::BakeCommand;
396    ///
397    /// let bake_cmd = BakeCommand::new()
398    ///     .no_cache();
399    /// ```
400    #[must_use]
401    pub fn no_cache(mut self) -> Self {
402        self.no_cache = true;
403        self
404    }
405
406    /// Print the options without building
407    ///
408    /// # Examples
409    ///
410    /// ```
411    /// use docker_wrapper::BakeCommand;
412    ///
413    /// let bake_cmd = BakeCommand::new()
414    ///     .print();
415    /// ```
416    #[must_use]
417    pub fn print(mut self) -> Self {
418        self.print = true;
419        self
420    }
421
422    /// Set type of progress output
423    ///
424    /// Valid values: "auto", "quiet", "plain", "tty", "rawjson"
425    ///
426    /// # Examples
427    ///
428    /// ```
429    /// use docker_wrapper::BakeCommand;
430    ///
431    /// let bake_cmd = BakeCommand::new()
432    ///     .progress("plain");
433    /// ```
434    #[must_use]
435    pub fn progress<S: Into<String>>(mut self, progress_type: S) -> Self {
436        self.progress = Some(progress_type.into());
437        self
438    }
439
440    /// Set provenance attestation (shorthand for --set=*.attest=type=provenance)
441    ///
442    /// # Examples
443    ///
444    /// ```
445    /// use docker_wrapper::BakeCommand;
446    ///
447    /// let bake_cmd = BakeCommand::new()
448    ///     .provenance("mode=max");
449    /// ```
450    #[must_use]
451    pub fn provenance<S: Into<String>>(mut self, provenance: S) -> Self {
452        self.provenance = Some(provenance.into());
453        self
454    }
455
456    /// Always attempt to pull all referenced images
457    ///
458    /// # Examples
459    ///
460    /// ```
461    /// use docker_wrapper::BakeCommand;
462    ///
463    /// let bake_cmd = BakeCommand::new()
464    ///     .pull();
465    /// ```
466    #[must_use]
467    pub fn pull(mut self) -> Self {
468        self.pull = true;
469        self
470    }
471
472    /// Push images to registry (shorthand for --set=*.output=type=registry)
473    ///
474    /// # Examples
475    ///
476    /// ```
477    /// use docker_wrapper::BakeCommand;
478    ///
479    /// let bake_cmd = BakeCommand::new()
480    ///     .push();
481    /// ```
482    #[must_use]
483    pub fn push(mut self) -> Self {
484        self.push = true;
485        self
486    }
487
488    /// Set SBOM attestation (shorthand for --set=*.attest=type=sbom)
489    ///
490    /// # Examples
491    ///
492    /// ```
493    /// use docker_wrapper::BakeCommand;
494    ///
495    /// let bake_cmd = BakeCommand::new()
496    ///     .sbom("generator=docker/buildkit");
497    /// ```
498    #[must_use]
499    pub fn sbom<S: Into<String>>(mut self, sbom: S) -> Self {
500        self.sbom = Some(sbom.into());
501        self
502    }
503
504    /// Override target value (e.g., "targetpattern.key=value")
505    ///
506    /// This allows overriding any target configuration at build time.
507    ///
508    /// # Examples
509    ///
510    /// ```
511    /// use docker_wrapper::BakeCommand;
512    ///
513    /// let bake_cmd = BakeCommand::new()
514    ///     .set("web.platform", "linux/amd64,linux/arm64")
515    ///     .set("*.output", "type=registry");
516    /// ```
517    #[must_use]
518    pub fn set<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
519        self.set_values.insert(key.into(), value.into());
520        self
521    }
522
523    /// Add multiple target value overrides
524    ///
525    /// # Examples
526    ///
527    /// ```
528    /// use std::collections::HashMap;
529    /// use docker_wrapper::BakeCommand;
530    ///
531    /// let mut overrides = HashMap::new();
532    /// overrides.insert("web.platform".to_string(), "linux/amd64".to_string());
533    /// overrides.insert("api.platform".to_string(), "linux/arm64".to_string());
534    ///
535    /// let bake_cmd = BakeCommand::new()
536    ///     .set_values(overrides);
537    /// ```
538    #[must_use]
539    pub fn set_values<I, K, V>(mut self, values: I) -> Self
540    where
541        I: IntoIterator<Item = (K, V)>,
542        K: Into<String>,
543        V: Into<String>,
544    {
545        self.set_values
546            .extend(values.into_iter().map(|(k, v)| (k.into(), v.into())));
547        self
548    }
549
550    /// Get the number of targets
551    ///
552    /// # Examples
553    ///
554    /// ```
555    /// use docker_wrapper::BakeCommand;
556    ///
557    /// let bake_cmd = BakeCommand::new()
558    ///     .target("web")
559    ///     .target("api");
560    ///
561    /// assert_eq!(bake_cmd.target_count(), 2);
562    /// ```
563    #[must_use]
564    pub fn target_count(&self) -> usize {
565        self.targets.len()
566    }
567
568    /// Get the list of targets
569    ///
570    /// # Examples
571    ///
572    /// ```
573    /// use docker_wrapper::BakeCommand;
574    ///
575    /// let bake_cmd = BakeCommand::new()
576    ///     .target("web")
577    ///     .target("api");
578    ///
579    /// let targets = bake_cmd.get_targets();
580    /// assert_eq!(targets, &["web", "api"]);
581    /// ```
582    #[must_use]
583    pub fn get_targets(&self) -> &[String] {
584        &self.targets
585    }
586
587    /// Get the list of build definition files
588    ///
589    /// # Examples
590    ///
591    /// ```
592    /// use docker_wrapper::BakeCommand;
593    ///
594    /// let bake_cmd = BakeCommand::new()
595    ///     .file("docker-compose.yml");
596    ///
597    /// let files = bake_cmd.get_files();
598    /// assert_eq!(files, &["docker-compose.yml"]);
599    /// ```
600    #[must_use]
601    pub fn get_files(&self) -> &[String] {
602        &self.files
603    }
604
605    /// Check if push mode is enabled
606    ///
607    /// # Examples
608    ///
609    /// ```
610    /// use docker_wrapper::BakeCommand;
611    ///
612    /// let bake_cmd = BakeCommand::new().push();
613    /// assert!(bake_cmd.is_push_enabled());
614    /// ```
615    #[must_use]
616    pub fn is_push_enabled(&self) -> bool {
617        self.push
618    }
619
620    /// Check if load mode is enabled
621    ///
622    /// # Examples
623    ///
624    /// ```
625    /// use docker_wrapper::BakeCommand;
626    ///
627    /// let bake_cmd = BakeCommand::new().load();
628    /// assert!(bake_cmd.is_load_enabled());
629    /// ```
630    #[must_use]
631    pub fn is_load_enabled(&self) -> bool {
632        self.load
633    }
634
635    /// Check if dry-run mode is enabled (print without building)
636    ///
637    /// # Examples
638    ///
639    /// ```
640    /// use docker_wrapper::BakeCommand;
641    ///
642    /// let bake_cmd = BakeCommand::new().print();
643    /// assert!(bake_cmd.is_dry_run());
644    /// ```
645    #[must_use]
646    pub fn is_dry_run(&self) -> bool {
647        self.print
648    }
649}
650
651impl Default for BakeCommand {
652    fn default() -> Self {
653        Self::new()
654    }
655}
656
657#[async_trait]
658impl DockerCommand for BakeCommand {
659    type Output = CommandOutput;
660
661    fn build_command_args(&self) -> Vec<String> {
662        let mut args = vec!["bake".to_string()];
663
664        // Add file arguments
665        for file in &self.files {
666            args.push("--file".to_string());
667            args.push(file.clone());
668        }
669
670        // Add allow arguments
671        for allow in &self.allow {
672            args.push("--allow".to_string());
673            args.push(allow.clone());
674        }
675
676        // Add builder
677        if let Some(ref builder) = self.builder {
678            args.push("--builder".to_string());
679            args.push(builder.clone());
680        }
681
682        // Add call method
683        if let Some(ref call) = self.call {
684            args.push("--call".to_string());
685            args.push(call.clone());
686        }
687
688        // Add check flag
689        if self.check {
690            args.push("--check".to_string());
691        }
692
693        // Add debug flag
694        if self.debug {
695            args.push("--debug".to_string());
696        }
697
698        // Add list
699        if let Some(ref list) = self.list {
700            args.push("--list".to_string());
701            args.push(list.clone());
702        }
703
704        // Add load flag
705        if self.load {
706            args.push("--load".to_string());
707        }
708
709        // Add metadata file
710        if let Some(ref metadata_file) = self.metadata_file {
711            args.push("--metadata-file".to_string());
712            args.push(metadata_file.clone());
713        }
714
715        // Add no-cache flag
716        if self.no_cache {
717            args.push("--no-cache".to_string());
718        }
719
720        // Add print flag
721        if self.print {
722            args.push("--print".to_string());
723        }
724
725        // Add progress
726        if let Some(ref progress) = self.progress {
727            args.push("--progress".to_string());
728            args.push(progress.clone());
729        }
730
731        // Add provenance
732        if let Some(ref provenance) = self.provenance {
733            args.push("--provenance".to_string());
734            args.push(provenance.clone());
735        }
736
737        // Add pull flag
738        if self.pull {
739            args.push("--pull".to_string());
740        }
741
742        // Add push flag
743        if self.push {
744            args.push("--push".to_string());
745        }
746
747        // Add sbom
748        if let Some(ref sbom) = self.sbom {
749            args.push("--sbom".to_string());
750            args.push(sbom.clone());
751        }
752
753        // Add set values
754        for (key, value) in &self.set_values {
755            args.push("--set".to_string());
756            args.push(format!("{key}={value}"));
757        }
758
759        // Add targets at the end
760        args.extend(self.targets.clone());
761        args.extend(self.executor.raw_args.clone());
762        args
763    }
764
765    async fn execute(&self) -> Result<Self::Output> {
766        let args = self.build_command_args();
767        let command_name = args[0].clone();
768        let command_args = args[1..].to_vec();
769        self.executor
770            .execute_command(&command_name, command_args)
771            .await
772    }
773
774    fn get_executor(&self) -> &CommandExecutor {
775        &self.executor
776    }
777
778    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
779        &mut self.executor
780    }
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786
787    #[test]
788    fn test_bake_command_basic() {
789        let bake_cmd = BakeCommand::new();
790        let args = bake_cmd.build_command_args();
791
792        assert_eq!(args, vec!["bake"]); // Only bake command for basic case
793    }
794
795    #[test]
796    fn test_bake_command_with_targets() {
797        let bake_cmd = BakeCommand::new()
798            .target("web")
799            .target("api")
800            .target("worker");
801
802        let args = bake_cmd.build_command_args();
803
804        assert!(args.contains(&"web".to_string()));
805        assert!(args.contains(&"api".to_string()));
806        assert!(args.contains(&"worker".to_string()));
807        assert_eq!(bake_cmd.target_count(), 3);
808    }
809
810    #[test]
811    fn test_bake_command_with_files() {
812        let bake_cmd = BakeCommand::new()
813            .file("docker-compose.yml")
814            .file("custom-bake.hcl");
815
816        let args = bake_cmd.build_command_args();
817
818        assert!(args.contains(&"--file".to_string()));
819        assert!(args.contains(&"docker-compose.yml".to_string()));
820        assert!(args.contains(&"custom-bake.hcl".to_string()));
821        assert_eq!(bake_cmd.get_files().len(), 2);
822    }
823
824    #[test]
825    fn test_bake_command_push_and_load() {
826        let bake_cmd = BakeCommand::new().push().load();
827
828        let args = bake_cmd.build_command_args();
829
830        assert!(args.contains(&"--push".to_string()));
831        assert!(args.contains(&"--load".to_string()));
832        assert!(bake_cmd.is_push_enabled());
833        assert!(bake_cmd.is_load_enabled());
834    }
835
836    #[test]
837    fn test_bake_command_with_builder() {
838        let bake_cmd = BakeCommand::new().builder("mybuilder");
839
840        let args = bake_cmd.build_command_args();
841
842        assert!(args.contains(&"--builder".to_string()));
843        assert!(args.contains(&"mybuilder".to_string()));
844    }
845
846    #[test]
847    fn test_bake_command_with_set_values() {
848        let bake_cmd = BakeCommand::new()
849            .set("web.platform", "linux/amd64,linux/arm64")
850            .set("*.output", "type=registry");
851
852        let args = bake_cmd.build_command_args();
853
854        assert!(args.contains(&"--set".to_string()));
855        assert!(args.contains(&"web.platform=linux/amd64,linux/arm64".to_string()));
856        assert!(args.contains(&"*.output=type=registry".to_string()));
857    }
858
859    #[test]
860    fn test_bake_command_flags() {
861        let bake_cmd = BakeCommand::new().check().debug().no_cache().print().pull();
862
863        let args = bake_cmd.build_command_args();
864
865        assert!(args.contains(&"--check".to_string()));
866        assert!(args.contains(&"--debug".to_string()));
867        assert!(args.contains(&"--no-cache".to_string()));
868        assert!(args.contains(&"--print".to_string()));
869        assert!(args.contains(&"--pull".to_string()));
870        assert!(bake_cmd.is_dry_run());
871    }
872
873    #[test]
874    fn test_bake_command_with_metadata_file() {
875        let bake_cmd = BakeCommand::new().metadata_file("build-metadata.json");
876
877        let args = bake_cmd.build_command_args();
878
879        assert!(args.contains(&"--metadata-file".to_string()));
880        assert!(args.contains(&"build-metadata.json".to_string()));
881    }
882
883    #[test]
884    fn test_bake_command_with_progress() {
885        let bake_cmd = BakeCommand::new().progress("plain");
886
887        let args = bake_cmd.build_command_args();
888
889        assert!(args.contains(&"--progress".to_string()));
890        assert!(args.contains(&"plain".to_string()));
891    }
892
893    #[test]
894    fn test_bake_command_with_attestations() {
895        let bake_cmd = BakeCommand::new()
896            .provenance("mode=max")
897            .sbom("generator=docker/buildkit");
898
899        let args = bake_cmd.build_command_args();
900
901        assert!(args.contains(&"--provenance".to_string()));
902        assert!(args.contains(&"mode=max".to_string()));
903        assert!(args.contains(&"--sbom".to_string()));
904        assert!(args.contains(&"generator=docker/buildkit".to_string()));
905    }
906
907    #[test]
908    fn test_bake_command_with_allow() {
909        let bake_cmd = BakeCommand::new()
910            .allow("network.host")
911            .allow("security.insecure");
912
913        let args = bake_cmd.build_command_args();
914
915        assert!(args.contains(&"--allow".to_string()));
916        assert!(args.contains(&"network.host".to_string()));
917        assert!(args.contains(&"security.insecure".to_string()));
918    }
919
920    #[test]
921    fn test_bake_command_with_call() {
922        let bake_cmd = BakeCommand::new().call("check");
923
924        let args = bake_cmd.build_command_args();
925
926        assert!(args.contains(&"--call".to_string()));
927        assert!(args.contains(&"check".to_string()));
928    }
929
930    #[test]
931    fn test_bake_command_with_list() {
932        let bake_cmd = BakeCommand::new().list("targets");
933
934        let args = bake_cmd.build_command_args();
935
936        assert!(args.contains(&"--list".to_string()));
937        assert!(args.contains(&"targets".to_string()));
938    }
939
940    #[test]
941    fn test_bake_command_extensibility() {
942        let mut bake_cmd = BakeCommand::new();
943        bake_cmd.get_executor_mut().add_arg("--experimental");
944        bake_cmd
945            .get_executor_mut()
946            .add_args(vec!["--custom", "value"]);
947
948        // Extensibility is handled through the executor's raw_args
949        // The actual testing of raw args is done in command.rs tests
950        // We can verify the executor methods are accessible
951        println!("Extensibility methods called successfully");
952    }
953}