From f2ed5573b2a46ed77f910a6adbf5a037136f8b01 Mon Sep 17 00:00:00 2001 From: Shikha Vyaghra Date: Thu, 2 Jan 2025 18:58:11 +0000 Subject: [PATCH 1/2] schnauzer: make resources public to access from Bootstrap container --- sources/api/schnauzer/src/v2/cli/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sources/api/schnauzer/src/v2/cli/mod.rs b/sources/api/schnauzer/src/v2/cli/mod.rs index f261ba085..c189a5563 100644 --- a/sources/api/schnauzer/src/v2/cli/mod.rs +++ b/sources/api/schnauzer/src/v2/cli/mod.rs @@ -13,7 +13,7 @@ use simplelog::{Config as LogConfig, LevelFilter, SimpleLogger}; use snafu::{OptionExt, ResultExt}; use std::path::PathBuf; -mod clirequirements; +pub mod clirequirements; /// Stores user-supplied arguments #[derive(Debug, FromArgs)] @@ -40,14 +40,14 @@ enum Subcommand { #[derive(Debug, FromArgs)] #[argh(subcommand, name = "render")] /// Render a template string -struct RenderArgs { +pub struct RenderArgs { /// extensions required to render this template, e.g. extension@version(helpers=[helper1, helper2]) #[argh(option)] - requires: Vec, + pub requires: Vec, /// template to render #[argh(option)] - template: String, + pub template: String, } #[derive(Debug, FromArgs)] From be3af6894299f43567fb3f0586399f9471161033 Mon Sep 17 00:00:00 2001 From: Shikha Vyaghra Date: Thu, 2 Jan 2025 18:48:46 +0000 Subject: [PATCH 2/2] bootstrap-containers: Add a option in bootstrap container binary This binary will be called by default from Sundog to populate default settings. This binary will fetch the available Bootstrap container settings and parse all the bootstrap container. If the source is not available, it adds the default source for that Bootstrap container else uses the source provided. --- packages/os/bootstrap-containers-toml | 2 + sources/Cargo.lock | 5 + sources/api/bootstrap-containers/Cargo.toml | 5 + sources/api/bootstrap-containers/src/main.rs | 161 ++++++++++++++++++- 4 files changed, 167 insertions(+), 6 deletions(-) diff --git a/packages/os/bootstrap-containers-toml b/packages/os/bootstrap-containers-toml index 3988328f4..bc5dc3fb8 100644 --- a/packages/os/bootstrap-containers-toml +++ b/packages/os/bootstrap-containers-toml @@ -5,7 +5,9 @@ std = { version = "v1", helpers = ["if_not_null"] } {{#if_not_null settings.bootstrap-containers}} {{#each settings.bootstrap-containers}} ["{{@key}}"] +{{#if_not_null this.source}} source = "{{{this.source}}}" +{{/if_not_null}} mode = "{{{this.mode}}}" {{#if_not_null this.user-data}} user-data = "{{{this.user-data}}}" diff --git a/sources/Cargo.lock b/sources/Cargo.lock index 6d1e108a2..519301b8d 100644 --- a/sources/Cargo.lock +++ b/sources/Cargo.lock @@ -1089,12 +1089,17 @@ dependencies = [ "base64 0.22.1", "bottlerocket-modeled-types", "constants", + "datastore", "generate-readme", "log", + "models", + "schnauzer", "serde", "serde_json", "simplelog", "snafu", + "tokio", + "tokio-util", "toml", ] diff --git a/sources/api/bootstrap-containers/Cargo.toml b/sources/api/bootstrap-containers/Cargo.toml index 980a2019d..bee9a1176 100644 --- a/sources/api/bootstrap-containers/Cargo.toml +++ b/sources/api/bootstrap-containers/Cargo.toml @@ -12,13 +12,18 @@ exclude = ["README.md"] [dependencies] base64.workspace = true constants.workspace = true +datastore.workspace = true log.workspace = true +models.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true simplelog.workspace = true snafu.workspace = true +tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread"] } +tokio-util = { workspace = true, features = ["compat", "io-util"] } toml.workspace = true bottlerocket-modeled-types.workspace = true +schnauzer.workspace = true [build-dependencies] generate-readme.workspace = true diff --git a/sources/api/bootstrap-containers/src/main.rs b/sources/api/bootstrap-containers/src/main.rs index b1d7c7e48..39959a0e7 100644 --- a/sources/api/bootstrap-containers/src/main.rs +++ b/sources/api/bootstrap-containers/src/main.rs @@ -74,6 +74,10 @@ journalctl -u bootstrap-containers@bear.service extern crate log; use base64::Engine; +use schnauzer::import::{SettingsResolver, TemplateImporter}; +use schnauzer::v2::cli::{clirequirements::CLIExtensionRequirement, RenderArgs}; +use schnauzer::v2::{ExtensionRequirement, Template, TemplateFrontmatter}; +use schnauzer::{render_template, BottlerocketTemplateImporter}; use serde::{Deserialize, Serialize}; use simplelog::{Config as LogConfig, LevelFilter, SimpleLogger}; use snafu::{ensure, OptionExt, ResultExt}; @@ -129,6 +133,7 @@ impl Default for Args { enum Subcommand { CreateContainers, MarkBootstrap(MarkBootstrapArgs), + RenderSource(RenderArgs), } #[derive(Debug)] @@ -186,7 +191,7 @@ fn parse_args(args: env::Args) -> Result<(Args, Subcommand)> { } // Subcommands - "create-containers" | "mark-bootstrap" + "create-containers" | "mark-bootstrap" | "render-source" if subcommand.is_none() && !arg.starts_with('-') => { subcommand = Some(arg) @@ -200,6 +205,7 @@ fn parse_args(args: env::Args) -> Result<(Args, Subcommand)> { match subcommand.as_deref() { Some("create-containers") => Ok((global_args, Subcommand::CreateContainers {})), Some("mark-bootstrap") => Ok((global_args, parse_mark_bootstrap_args(subcommand_args)?)), + Some("render-source") => Ok((global_args, parse_render_source_args(subcommand_args)?)), None => error::UsageSnafu { message: "Missing subcommand".to_string(), } @@ -211,6 +217,48 @@ fn parse_args(args: env::Args) -> Result<(Args, Subcommand)> { } } +fn parse_render_source_args(args: Vec) -> Result { + let mut requires = Vec::new(); + let mut template = None; + + let mut iter = args.into_iter(); + while let Some(arg) = iter.next() { + match arg.as_ref() { + "--requires" => { + let cli_extention_requirement = + CLIExtensionRequirement::from_str(&iter.next().context(error::UsageSnafu { + message: "Did not give argument to --requires", + })?) + .context(error::CLIExtensionConversionSnafu)?; + + requires.push(cli_extention_requirement); + } + "--template" => { + template = Some(iter.next().context(error::UsageSnafu { + message: "Did not give argument to --template", + })?); + } + x => { + return error::UsageSnafu { + message: format!("Unexpected argument '{}'", x), + } + .fail() + } + } + } + + let bootstrap_container_requires = CLIExtensionRequirement::from_str("bootstrap-containers@v1") + .context(error::CLIExtensionConversionSnafu)?; + requires.push(bootstrap_container_requires); + + Ok(Subcommand::RenderSource(RenderArgs { + requires, + template: template.context(error::UsageSnafu { + message: "Did not give argument to --template".to_string(), + })?, + })) +} + /// Parses arguments for the 'mark-bootstrap' subcommand fn parse_mark_bootstrap_args(args: Vec) -> Result { let mut container_id = None; @@ -497,6 +545,8 @@ fn create_containers

(config_path: P) -> Result<()> where P: AsRef, { + info!("bootstrap-containers started"); + let mut failed = 0usize; let bootstrap_containers = get_bootstrap_containers(config_path)?; for (name, container_details) in bootstrap_containers.iter() { @@ -522,6 +572,8 @@ where /// container's systemd unit, which could potentially cause a concurrent invocation /// in this binary after the API setting finalizes. fn mark_bootstrap(args: MarkBootstrapArgs) -> Result<()> { + info!("bootstrap-containers started"); + let container_id: &str = args.container_id.as_ref(); let mode = args.mode.as_ref(); info!("Mode for '{}' is '{}'", container_id, mode); @@ -538,25 +590,94 @@ fn mark_bootstrap(args: MarkBootstrapArgs) -> Result<()> { Ok(()) } -fn run() -> Result<()> { +async fn render_source(render_args: RenderArgs) -> Result<()> { + // Create template using the render args + let template_importer = BottlerocketTemplateImporter::new(constants::API_SOCKET.into()); + + let frontmatter: TemplateFrontmatter = render_args + .requires + .into_iter() + .map(Into::::into) + .collect::>() + .try_into() + .context(error::FrontmatterParseSnafu)?; + + let template = Template { + frontmatter, + body: render_args.template, + }; + + // Get the default Bootstrap container source using template + let default_source = render_template(&template_importer, &template) + .await + .context(error::RenderTemplateSnafu)?; + + // Fetch all the available settings and extract Bootstrap container settings + let settings_with_os = template_importer + .settings_resolver() + .fetch_settings(template.frontmatter.extension_requirements()) + .await + .context(error::RetrieveSettingsSnafu)?; + + let bootstrap_containers = settings_with_os + .as_object() + .and_then(|obj| obj.get("settings")) + .and_then(|settings| settings.get("bootstrap-containers").cloned()) + .and_then(|bootstrap_container| bootstrap_container.as_object().cloned()); + + // Add the source for the Bootstrap container where source has not been provided. + let mut result: HashMap> = HashMap::new(); + if let Some(found_bootstrap_containers) = bootstrap_containers { + for (bootstrap_container_name, container_props) in found_bootstrap_containers.iter() { + let bootstrap_container: BootstrapContainer = serde_json::from_value( + container_props.clone(), + ) + .context(error::JsonDeserilizationSnafu { + object: "bootstrap_container".to_string(), + })?; + + let source = match bootstrap_container.source { + Some(source) => source, + None => { + Url::try_from(default_source.clone()).context(error::UrlFormationSnafu { + url: default_source.clone(), + })? + } + }; + result.insert( + bootstrap_container_name.to_string(), + HashMap::from([("source".to_owned(), source)]), + ); + } + } + + println!( + "{}", + serde_json::to_value(&result).context(error::JSONSerializationSnafu)? + ); + + Ok(()) +} + +async fn run() -> Result<()> { let (args, subcommand) = parse_args(env::args())?; // SimpleLogger will send errors to stderr and anything less to stdout. SimpleLogger::init(args.log_level, LogConfig::default()).context(error::LoggerSnafu)?; - info!("bootstrap-containers started"); - match subcommand { Subcommand::CreateContainers => create_containers(args.config_path), Subcommand::MarkBootstrap(mark_bootstrap_args) => mark_bootstrap(mark_bootstrap_args), + Subcommand::RenderSource(render_args) => render_source(render_args).await, } } // Returning a Result from main makes it print a Debug representation of the error, but with Snafu // we have nice Display representations of the error, so we wrap "main" (run) and print any error. // https://github.com/shepmaster/snafu/issues/110 -fn main() { - if let Err(e) = run() { +#[tokio::main] +async fn main() { + if let Err(e) = run().await { match e { error::Error::Usage { .. } => { eprintln!("{}", e); @@ -574,6 +695,7 @@ fn main() { // =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= mod error { + use schnauzer::v2::cli::CLIError; use snafu::Snafu; use std::fmt; use std::io; @@ -660,6 +782,33 @@ mod error { #[snafu(display("Failed write value '{}': {}", value, source))] WriteConfigurationValue { value: String, source: fmt::Error }, + + #[snafu(display("Failed to convert given string to CLI extension from {}", source))] + CLIExtensionConversion { source: CLIError }, + + #[snafu(display("Failed to parse template requirements: '{}'", source))] + FrontmatterParse { source: schnauzer::template::Error }, + + #[snafu(display("Failed to render template: '{}'", source))] + RenderTemplate { source: schnauzer::RenderError }, + + #[snafu(display("Failed to retrieve settings from Bottlerocket API: {}", source))] + RetrieveSettings { source: Box }, + + #[snafu(display("Failed to convert JSON value to {}: {}", object, source))] + JsonDeserilization { + object: String, + source: serde_json::Error, + }, + + #[snafu(display("Failed to convert object to URL {}", url))] + UrlFormation { + url: String, + source: bottlerocket_modeled_types::error::Error, + }, + + #[snafu(display("Failed to serialize JSON: {}", source))] + JSONSerialization { source: serde_json::Error }, } }