Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable rendering bootstrap source #335

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/os/bootstrap-containers-toml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}}"
Expand Down
5 changes: 5 additions & 0 deletions sources/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions sources/api/bootstrap-containers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
161 changes: 155 additions & 6 deletions sources/api/bootstrap-containers/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ journalctl -u [email protected]
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};
Expand Down Expand Up @@ -129,6 +133,7 @@ impl Default for Args {
enum Subcommand {
CreateContainers,
MarkBootstrap(MarkBootstrapArgs),
RenderSource(RenderArgs),
}

#[derive(Debug)]
Expand Down Expand Up @@ -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)
Expand All @@ -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(),
}
Expand All @@ -211,6 +217,48 @@ fn parse_args(args: env::Args) -> Result<(Args, Subcommand)> {
}
}

fn parse_render_source_args(args: Vec<String>) -> Result<Subcommand> {
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<String>) -> Result<Subcommand> {
let mut container_id = None;
Expand Down Expand Up @@ -497,6 +545,8 @@ fn create_containers<P>(config_path: P) -> Result<()>
where
P: AsRef<Path>,
{
info!("bootstrap-containers started");

let mut failed = 0usize;
let bootstrap_containers = get_bootstrap_containers(config_path)?;
for (name, container_details) in bootstrap_containers.iter() {
Expand All @@ -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);
Expand All @@ -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::<ExtensionRequirement>::into)
.collect::<Vec<_>>()
.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<String, HashMap<String, Url>> = 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);
Expand All @@ -574,6 +695,7 @@ fn main() {
// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=

mod error {
use schnauzer::v2::cli::CLIError;
use snafu::Snafu;
use std::fmt;
use std::io;
Expand Down Expand Up @@ -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<dyn std::error::Error> },

#[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 },
}
}

Expand Down
8 changes: 4 additions & 4 deletions sources/api/schnauzer/src/v2/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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<CLIExtensionRequirement>,
pub requires: Vec<CLIExtensionRequirement>,

/// template to render
#[argh(option)]
template: String,
pub template: String,
}

#[derive(Debug, FromArgs)]
Expand Down
Loading