blue_build/commands/
generate.rs

1use std::{
2    env,
3    ops::Not,
4    path::{Path, PathBuf},
5};
6
7use blue_build_process_management::drivers::{
8    CiDriver, Driver, DriverArgs, InspectDriver, opts::GetMetadataOpts, types::Platform,
9};
10use blue_build_recipe::Recipe;
11use blue_build_template::{ContainerFileTemplate, Template};
12use blue_build_utils::{
13    constants::{
14        BB_SKIP_VALIDATION, BUILD_SCRIPTS_IMAGE_REF, CONFIG_PATH, RECIPE_FILE, RECIPE_PATH,
15    },
16    syntax_highlighting::{self, DefaultThemes},
17};
18use bon::Builder;
19use cached::proc_macro::cached;
20use clap::{Args, crate_version};
21use log::{debug, info, trace, warn};
22use miette::{IntoDiagnostic, Result};
23use oci_distribution::Reference;
24
25use crate::{commands::validate::ValidateCommand, shadow};
26
27use super::BlueBuildCommand;
28
29#[derive(Debug, Clone, Args, Builder)]
30pub struct GenerateCommand {
31    /// The recipe file to create a template from
32    #[arg()]
33    #[builder(into)]
34    recipe: Option<PathBuf>,
35
36    /// File to output to instead of STDOUT
37    #[arg(short, long)]
38    #[builder(into)]
39    output: Option<PathBuf>,
40
41    /// The registry domain the image will be published to.
42    ///
43    /// This is used for modules that need to know where
44    /// the image is being published (i.e. the signing module).
45    #[arg(long)]
46    #[builder(into)]
47    registry: Option<String>,
48
49    /// The registry namespace the image will be published to.
50    ///
51    /// This is used for modules that need to know where
52    /// the image is being published (i.e. the signing module).
53    #[arg(long)]
54    #[builder(into)]
55    registry_namespace: Option<String>,
56
57    /// Instead of creating a Containerfile, display
58    /// the full recipe after traversing all `from-file` properties.
59    ///
60    /// This can be used to help debug the order
61    /// you defined your recipe.
62    #[arg(short, long)]
63    #[builder(default)]
64    display_full_recipe: bool,
65
66    /// Choose a theme for the syntax highlighting
67    /// for the Containerfile or Yaml.
68    ///
69    /// The default is `mocha-dark`.
70    #[arg(short = 't', long)]
71    syntax_theme: Option<DefaultThemes>,
72
73    /// Inspect the image for a specific platform
74    /// when retrieving the version.
75    #[arg(long, default_value = "native")]
76    platform: Option<Platform>,
77
78    /// Skips validation of the recipe file.
79    #[arg(long, env = BB_SKIP_VALIDATION)]
80    #[builder(default)]
81    skip_validation: bool,
82
83    #[clap(flatten)]
84    #[builder(default)]
85    drivers: DriverArgs,
86}
87
88impl BlueBuildCommand for GenerateCommand {
89    fn try_run(&mut self) -> Result<()> {
90        Driver::init(self.drivers);
91
92        self.template_file()
93    }
94}
95
96impl GenerateCommand {
97    fn template_file(&self) -> Result<()> {
98        trace!("TemplateCommand::template_file()");
99
100        let recipe_path = self.recipe.clone().unwrap_or_else(|| {
101            let legacy_path = Path::new(CONFIG_PATH);
102            let recipe_path = Path::new(RECIPE_PATH);
103            if recipe_path.exists() && recipe_path.is_dir() {
104                recipe_path.join(RECIPE_FILE)
105            } else {
106                warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}");
107                legacy_path.join(RECIPE_FILE)
108            }
109        });
110
111        if self.skip_validation.not() {
112            ValidateCommand::builder()
113                .recipe(recipe_path.clone())
114                .build()
115                .try_run()?;
116        }
117
118        let registry = if let (Some(registry), Some(registry_namespace)) =
119            (&self.registry, &self.registry_namespace)
120        {
121            format!("{registry}/{registry_namespace}")
122        } else {
123            Driver::get_registry()?
124        };
125
126        debug!("Deserializing recipe");
127        let recipe = Recipe::parse(&recipe_path)?;
128        trace!("recipe_de: {recipe:#?}");
129
130        if self.display_full_recipe {
131            if let Some(output) = self.output.as_ref() {
132                std::fs::write(output, serde_yaml::to_string(&recipe).into_diagnostic()?)
133                    .into_diagnostic()?;
134            } else {
135                syntax_highlighting::print_ser(&recipe, "yml", self.syntax_theme)?;
136            }
137            return Ok(());
138        }
139
140        info!("Templating for recipe at {}", recipe_path.display());
141
142        let base_image: Reference = format!("{}:{}", &recipe.base_image, &recipe.image_version)
143            .parse()
144            .into_diagnostic()?;
145
146        let template = ContainerFileTemplate::builder()
147            .os_version(
148                Driver::get_os_version()
149                    .oci_ref(&recipe.base_image_ref()?)
150                    .maybe_platform(self.platform)
151                    .call()?,
152            )
153            .build_id(Driver::get_build_id())
154            .recipe(&recipe)
155            .recipe_path(recipe_path.as_path())
156            .registry(registry)
157            .repo(Driver::get_repo_url()?)
158            .build_scripts_image(determine_scripts_tag(self.platform)?.to_string())
159            .base_digest(
160                Driver::get_metadata(
161                    &GetMetadataOpts::builder()
162                        .image(&base_image)
163                        .maybe_platform(self.platform)
164                        .build(),
165                )?
166                .digest,
167            )
168            .maybe_nushell_version(recipe.nushell_version.as_ref())
169            .build();
170
171        let output_str = template.render().into_diagnostic()?;
172        if let Some(output) = self.output.as_ref() {
173            debug!("Templating to file {}", output.display());
174            trace!("Containerfile:\n{output_str}");
175
176            std::fs::write(output, output_str).into_diagnostic()?;
177        } else {
178            debug!("Templating to stdout");
179            syntax_highlighting::print(&output_str, "Dockerfile", self.syntax_theme)?;
180        }
181
182        Ok(())
183    }
184}
185
186#[cached(
187    result = true,
188    key = "Option<Platform>",
189    convert = r#"{ platform }"#,
190    sync_writes = "by_key"
191)]
192fn determine_scripts_tag(platform: Option<Platform>) -> Result<Reference> {
193    trace!("determine_scripts_tag({platform:?})");
194
195    let opts = GetMetadataOpts::builder().maybe_platform(platform);
196    format!("{BUILD_SCRIPTS_IMAGE_REF}:{}", shadow::COMMIT_HASH)
197        .parse()
198        .into_diagnostic()
199        .and_then(|image| {
200            Driver::get_metadata(&opts.clone().image(&image).build())
201                .inspect_err(|e| trace!("{e:?}"))
202                .map(|_| image)
203        })
204        .or_else(|_| {
205            let image: Reference = format!("{BUILD_SCRIPTS_IMAGE_REF}:{}", shadow::BRANCH)
206                .parse()
207                .into_diagnostic()?;
208            Driver::get_metadata(&opts.clone().image(&image).build())
209                .inspect_err(|e| trace!("{e:?}"))
210                .map(|_| image)
211        })
212        .or_else(|_| {
213            let image: Reference = format!("{BUILD_SCRIPTS_IMAGE_REF}:v{}", crate_version!())
214                .parse()
215                .into_diagnostic()?;
216            Driver::get_metadata(&opts.image(&image).build())
217                .inspect_err(|e| trace!("{e:?}"))
218                .map(|_| image)
219        })
220        .inspect(|image| debug!("Using build scripts image: {image}"))
221}