feat(cli): add environment variable support and restructure commands
- Add HBW_USERNAME, HBW_PIPELINE, HBW_BUILD_ID, HBW_DRY_RUN env vars - Wrap prebake/bake/finalize in Bare subcommand for cleaner hierarchy - Remove unimplemented Monitor command
This commit is contained in:
@@ -24,7 +24,7 @@ anyhow = "1.0.100"
|
||||
bollard = { version = "0.20.0", features = ["buildkit", "chrono", "ssh"] }
|
||||
cgroups-rs = "0.5.0"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
clap = { version = "4.5.53", features = ["derive", "env"] }
|
||||
config = "0.15.19"
|
||||
duration-str = "0.21.0"
|
||||
env_logger = "0.11.8"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! which handles artifact collection, publishing, and notifications.
|
||||
|
||||
use crate::types::command::CustomCommand;
|
||||
use crate::types::compression::CompressionMethod;
|
||||
use crate::types::time::deserialize_duration_ms;
|
||||
use semver::{Version, VersionReq};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -102,6 +103,33 @@ pub struct NotificationMethod {
|
||||
#[serde(default)]
|
||||
pub fallible: bool,
|
||||
|
||||
/// Number of retries on failure (0 = no retry, -1 = infinite retries)
|
||||
#[serde(default)]
|
||||
pub retry: i8,
|
||||
|
||||
/// Timeout for notification execution in milliseconds (0 = no timeout)
|
||||
#[serde(default)]
|
||||
pub timeout_ms: u64,
|
||||
|
||||
/// Template definition
|
||||
#[serde(default)]
|
||||
pub template: NotificationTemplate,
|
||||
|
||||
/// Trigger definition
|
||||
#[serde(default)]
|
||||
pub trigger: Vec<TriggerItemRaw>,
|
||||
|
||||
/// Notification-specific configuration (url, token, to, from, etc.)
|
||||
#[serde(flatten)]
|
||||
pub config: Value,
|
||||
|
||||
/// Policy list for flexible trigger/template/config combinations.
|
||||
#[serde(default)]
|
||||
pub policy: Vec<NotifyPolicy>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
pub struct NotificationTemplate {
|
||||
/// Payload schema: "default" (plugin-defined) or "custom" (requires on_* templates)
|
||||
#[serde(default)]
|
||||
pub schema: Option<String>,
|
||||
@@ -117,10 +145,6 @@ pub struct NotificationMethod {
|
||||
/// Stage-triggered notification configuration
|
||||
#[serde(default)]
|
||||
pub on_finish_of: Option<OnFinishOf>,
|
||||
|
||||
/// Notification-specific configuration (url, token, to, from, etc.)
|
||||
#[serde(flatten)]
|
||||
pub config: Value,
|
||||
}
|
||||
|
||||
/// Notification configuration container
|
||||
@@ -128,9 +152,8 @@ pub struct NotificationMethod {
|
||||
pub struct NotificationConfig {
|
||||
/// Template definitions for notification content
|
||||
#[serde(default)]
|
||||
pub templates: HashMap<String, HashMap<String, PluginConfig>>,
|
||||
pub templates: HashMap<String, NotificationTemplateDef>,
|
||||
|
||||
/// Priority groups for notification delivery
|
||||
#[serde(flatten)]
|
||||
pub groups: HashMap<u32, HashMap<String, NotificationMethod>>,
|
||||
}
|
||||
@@ -146,6 +169,63 @@ pub struct OnFinishOf {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
/// Notification template definition.
|
||||
/// Supports inline content (on_success/on_failure/on_finish_of) or external reference (use).
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct NotificationTemplateDef {
|
||||
/// External template reference (git URL).
|
||||
/// Cannot be combined with on_* fields in MVP (validation will reject).
|
||||
#[serde(default, rename = "use")]
|
||||
pub use_: Option<String>,
|
||||
|
||||
/// Content template for build success notification
|
||||
#[serde(default)]
|
||||
pub on_success: Option<String>,
|
||||
|
||||
/// Content template for build failure notification
|
||||
#[serde(default)]
|
||||
pub on_failure: Option<String>,
|
||||
|
||||
/// Content template for stage completion notification
|
||||
#[serde(default)]
|
||||
pub on_finish_of: Option<String>,
|
||||
}
|
||||
|
||||
/// Notification trigger type.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Trigger {
|
||||
OnSuccess,
|
||||
OnFailure,
|
||||
OnFinishOf(String),
|
||||
}
|
||||
|
||||
/// Raw trigger item for deserialization.
|
||||
/// Supports `- on_success` (string) and `- on_finish_of: [...]` (map).
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum TriggerItemRaw {
|
||||
Simple(String),
|
||||
Map(std::collections::HashMap<String, Vec<String>>),
|
||||
}
|
||||
|
||||
/// Notification policy: a set of triggers with optional template/config overrides.
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct NotifyPolicy {
|
||||
/// Trigger conditions for this policy.
|
||||
/// Empty means inheriting method-level triggers.
|
||||
#[serde(default)]
|
||||
pub trigger: Vec<TriggerItemRaw>,
|
||||
|
||||
/// Template configuration (schema, on_success, on_failure, on_finish_of, subject, body...).
|
||||
/// Empty/null means inheriting method-level templates.
|
||||
#[serde(default)]
|
||||
pub template: Value,
|
||||
|
||||
/// Override fields: to, url, token, fallible, retry, timeout_ms, etc.
|
||||
#[serde(flatten)]
|
||||
pub overrides: Value,
|
||||
}
|
||||
|
||||
/// Artifact definition
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ArtifactDef {
|
||||
@@ -167,16 +247,11 @@ pub struct ArtifactDef {
|
||||
pub retention_ms: u64,
|
||||
|
||||
/// Compression method: "zstd", "gzip", "none", "dir"
|
||||
#[serde(default = "default_compression")]
|
||||
#[serde(default)]
|
||||
pub compression: CompressionMethod,
|
||||
|
||||
/// Trigger conditions: "success", "failure", "always" (WIP: failure, always)
|
||||
#[serde(default = "default_on_conditions")]
|
||||
pub on: Vec<ArtifactCondition>,
|
||||
|
||||
/// Publishing targets
|
||||
#[serde(default)]
|
||||
pub publish: HashMap<String, PublishTarget>,
|
||||
// Publishing targets
|
||||
// TODO: under heavy refactor
|
||||
}
|
||||
|
||||
impl Default for ArtifactDef {
|
||||
@@ -185,55 +260,19 @@ impl Default for ArtifactDef {
|
||||
id: None,
|
||||
path: String::new(),
|
||||
retention_ms: default_retention_ms(),
|
||||
compression: default_compression(),
|
||||
on: default_on_conditions(),
|
||||
publish: HashMap::new(),
|
||||
compression: CompressionMethod::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compression method for artifacts
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CompressionMethod {
|
||||
#[default]
|
||||
Zstd,
|
||||
Gzip,
|
||||
None,
|
||||
Dir,
|
||||
}
|
||||
|
||||
/// Artifact trigger condition
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ArtifactCondition {
|
||||
Success,
|
||||
Failure,
|
||||
Always,
|
||||
}
|
||||
|
||||
/// Publish target configuration
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct PublishTarget {
|
||||
/// Whether publish failure should not fail the build
|
||||
#[serde(default)]
|
||||
pub fallible: bool,
|
||||
|
||||
/// Conditional execution (WIP)
|
||||
/// Format: "{{ env.BRANCH }} == 'main'"
|
||||
#[serde(rename = "if", default)]
|
||||
pub condition: Option<String>,
|
||||
|
||||
/// Plugin-specific configuration (image, bucket, etc.)
|
||||
#[serde(flatten)]
|
||||
pub config: Value,
|
||||
}
|
||||
/// Artifact
|
||||
/// TODO: Under heavy refactor
|
||||
|
||||
/// Cleanup configuration
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
pub struct CleanupConfig {
|
||||
/// Cleanup policy: "auto", "manual", "on_failure" (WIP: manual, on_failure)
|
||||
#[serde(default = "default_cleanup_policy")]
|
||||
#[serde(default)]
|
||||
pub policy: CleanupPolicy,
|
||||
}
|
||||
|
||||
@@ -244,7 +283,8 @@ pub enum CleanupPolicy {
|
||||
#[default]
|
||||
Auto,
|
||||
Manual,
|
||||
OnFailure,
|
||||
OnSuccess,
|
||||
Never,
|
||||
}
|
||||
|
||||
/// Hooks configuration for finalize stage
|
||||
@@ -264,18 +304,6 @@ fn default_retention_ms() -> u64 {
|
||||
7 * 24 * 3600 * 1000
|
||||
}
|
||||
|
||||
fn default_compression() -> CompressionMethod {
|
||||
CompressionMethod::Zstd
|
||||
}
|
||||
|
||||
fn default_on_conditions() -> Vec<ArtifactCondition> {
|
||||
vec![ArtifactCondition::Success]
|
||||
}
|
||||
|
||||
fn default_cleanup_policy() -> CleanupPolicy {
|
||||
CleanupPolicy::Auto
|
||||
}
|
||||
|
||||
impl FinalizeConfig {
|
||||
/// Validate the finalize configuration
|
||||
pub fn validate(&self) -> Result<(), Vec<String>> {
|
||||
@@ -311,8 +339,7 @@ impl FinalizeConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Schema-based validation for plugin configs
|
||||
// TODO: Validate that publish targets reference existing plugins
|
||||
self.validate_notification(&mut errors);
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
@@ -320,136 +347,16 @@ impl FinalizeConfig {
|
||||
Err(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_hooks_full() {
|
||||
let yaml = r#"
|
||||
version: "0.0.1"
|
||||
hooks:
|
||||
early:
|
||||
- name: "test-early"
|
||||
command: "echo early"
|
||||
timeout: 10
|
||||
late:
|
||||
- name: "test-late"
|
||||
command: "plugin:test-plugin"
|
||||
working_dir: "/tmp"
|
||||
"#;
|
||||
let config: FinalizeConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.hooks.is_some());
|
||||
let hooks = config.hooks.unwrap();
|
||||
let early = hooks.early.as_ref().unwrap();
|
||||
let late = hooks.late.as_ref().unwrap();
|
||||
assert_eq!(early.len(), 1);
|
||||
assert_eq!(late.len(), 1);
|
||||
assert_eq!(early[0].command, "echo early");
|
||||
assert_eq!(late[0].command, "plugin:test-plugin");
|
||||
assert_eq!(late[0].working_dir, Some("/tmp".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_hooks_missing() {
|
||||
let yaml = r#"
|
||||
version: "0.0.1"
|
||||
"#;
|
||||
let config: FinalizeConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.hooks.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_hooks_timeout_float() {
|
||||
let yaml = r#"
|
||||
version: "0.0.1"
|
||||
hooks:
|
||||
early:
|
||||
- name: "float-timeout"
|
||||
command: "echo ok"
|
||||
timeout: 10.5
|
||||
"#;
|
||||
let config: FinalizeConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
let hooks = config.hooks.unwrap();
|
||||
let early = hooks.early.unwrap();
|
||||
assert_eq!(early[0].timeout_ms, 10500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_hooks_timeout_string() {
|
||||
let yaml = r#"
|
||||
version: "0.0.1"
|
||||
hooks:
|
||||
early:
|
||||
- name: "string-timeout"
|
||||
command: "echo ok"
|
||||
timeout: "30s"
|
||||
"#;
|
||||
let config: FinalizeConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
let hooks = config.hooks.unwrap();
|
||||
let early = hooks.early.unwrap();
|
||||
assert_eq!(early[0].timeout_ms, 30000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_plugin_zero_urls() {
|
||||
let config: FinalizeConfig = serde_yaml::from_str(
|
||||
r#"
|
||||
version: "0.0.1"
|
||||
plugin:
|
||||
show_file_list: {}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = config.validate();
|
||||
assert!(result.is_err());
|
||||
let errors = result.unwrap_err();
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.any(|e| e.contains("show_file_list") && e.contains("no URL"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_plugin_one_url_ok() {
|
||||
let config: FinalizeConfig = serde_yaml::from_str(
|
||||
r#"
|
||||
version: "0.0.1"
|
||||
plugin:
|
||||
show_file_list:
|
||||
"https://example.com/plugin.tar.gz":
|
||||
fallible: false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = config.validate();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_plugin_multiple_urls() {
|
||||
let config: FinalizeConfig = serde_yaml::from_str(
|
||||
r#"
|
||||
version: "0.0.1"
|
||||
plugin:
|
||||
show_file_list:
|
||||
"https://example.com/plugin1.tar.gz":
|
||||
fallible: false
|
||||
"https://example.com/plugin2.tar.gz":
|
||||
fallible: false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = config.validate();
|
||||
assert!(result.is_err());
|
||||
let errors = result.unwrap_err();
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.any(|e| e.contains("show_file_list") && e.contains("2 URL definitions"))
|
||||
);
|
||||
fn validate_notification(&self, errors: &mut Vec<String>) {
|
||||
// Validate templates
|
||||
for (name, tmpl) in &self.notification.templates {
|
||||
if tmpl.use_.is_some() && (tmpl.on_success.is_some() || tmpl.on_failure.is_some() || tmpl.on_finish_of.is_some()) {
|
||||
errors.push(format!(
|
||||
"Template '{}' cannot combine 'use' with 'on_*' fields (MVP restriction)",
|
||||
name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,19 +34,19 @@ pub mod cli {
|
||||
#[command(version, about, long_about=None)]
|
||||
pub struct Cli {
|
||||
/// Username of the pipeline executor
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, env = "HBW_USERNAME")]
|
||||
pub username: String,
|
||||
|
||||
/// Pipeline ID of this pipeline
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, env = "HBW_PIPELINE")]
|
||||
pub pipeline: String,
|
||||
|
||||
/// Build ID of this build
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, env = "HBW_BUILD_ID")]
|
||||
pub build_id: String,
|
||||
|
||||
/// Parse the script without executing
|
||||
#[arg(long, default_value = "false")]
|
||||
#[arg(long, default_value = "false", env = "HBW_DRY_RUN")]
|
||||
pub dry_run: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
@@ -55,12 +55,21 @@ pub mod cli {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Commands {
|
||||
/// Monitor a running task
|
||||
Monitor { group: String },
|
||||
/// Prepare a task
|
||||
Prebake { config: PathBuf },
|
||||
/// Run pipeline stages in standalone (bare) mode
|
||||
Bare {
|
||||
#[command(subcommand)]
|
||||
command: BareCommands,
|
||||
},
|
||||
|
||||
/// Run in client mode
|
||||
Client { config: String },
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum BareCommands {
|
||||
/// Prepare a task
|
||||
Prebake { config: PathBuf },
|
||||
|
||||
/// Run a task
|
||||
Bake {
|
||||
/// Path to the script to execute
|
||||
@@ -71,6 +80,7 @@ pub mod cli {
|
||||
#[arg(short, long, default_value = "./prebake.yml")]
|
||||
prebake: PathBuf,
|
||||
},
|
||||
|
||||
/// Finish a task
|
||||
Finalize { config: String },
|
||||
}
|
||||
|
||||
+100
-98
@@ -2,7 +2,7 @@ use std::path::Path;
|
||||
|
||||
use workshop_baker::{
|
||||
bake::bake,
|
||||
cli::{Cli, Commands, Parser},
|
||||
cli::{BareCommands, Cli, Commands, Parser},
|
||||
daemon,
|
||||
engine::{EventSender, ExecutionContext},
|
||||
error::PrebakeError,
|
||||
@@ -18,111 +18,113 @@ async fn main() {
|
||||
log::warn!("Dry run enabled, no changes will be made.");
|
||||
}
|
||||
match &cli.command {
|
||||
Some(Commands::Monitor { group: _ }) => {
|
||||
unimplemented!("executor-monitor should be moved to daemon instead.")
|
||||
}
|
||||
Some(Commands::Client { config: _ }) => {
|
||||
unimplemented!("Client mode not yet implemented.")
|
||||
}
|
||||
Some(Commands::Prebake { config }) => {
|
||||
log::info!("Running in standalone mode.");
|
||||
let mut ctx = ExecutionContext {
|
||||
standalone: true,
|
||||
task_id: format!("{}-{}-init", cli.pipeline, cli.build_id),
|
||||
pipeline_name: cli.pipeline.clone(),
|
||||
username: cli.username.clone(),
|
||||
dry_run: cli.dry_run,
|
||||
privileged: true,
|
||||
..Default::default()
|
||||
};
|
||||
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
|
||||
let event_tx: EventSender = Some(tx);
|
||||
let pipeline_name = cli.pipeline.clone();
|
||||
let build_id = cli.build_id.clone();
|
||||
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||
event_rx,
|
||||
pipeline_name,
|
||||
build_id,
|
||||
));
|
||||
let result = prebake(config, &cli, None, &mut ctx, event_tx).await;
|
||||
drop(ctx);
|
||||
let _ = receiver_handle.await;
|
||||
dbg!(&result);
|
||||
if let Err(ref e) = result {
|
||||
log::error!("Prebake stage failed: {}", e);
|
||||
std::process::exit(
|
||||
e.downcast_ref::<PrebakeError>()
|
||||
.map(|pe| pe.exit_code())
|
||||
.unwrap_or(1),
|
||||
);
|
||||
Some(Commands::Bare { command }) => match command {
|
||||
BareCommands::Prebake { config } => {
|
||||
log::info!("Running in standalone mode.");
|
||||
let mut ctx = ExecutionContext {
|
||||
standalone: true,
|
||||
task_id: format!("{}-{}-init", cli.pipeline, cli.build_id),
|
||||
pipeline_name: cli.pipeline.clone(),
|
||||
username: cli.username.clone(),
|
||||
dry_run: cli.dry_run,
|
||||
privileged: true,
|
||||
..Default::default()
|
||||
};
|
||||
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
|
||||
let event_tx: EventSender = Some(tx);
|
||||
let pipeline_name = cli.pipeline.clone();
|
||||
let build_id = cli.build_id.clone();
|
||||
let receiver_handle =
|
||||
tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||
event_rx,
|
||||
pipeline_name,
|
||||
build_id,
|
||||
));
|
||||
let result = prebake(config, &cli, None, &mut ctx, event_tx).await;
|
||||
drop(ctx);
|
||||
let _ = receiver_handle.await;
|
||||
dbg!(&result);
|
||||
if let Err(ref e) = result {
|
||||
log::error!("Prebake stage failed: {}", e);
|
||||
std::process::exit(
|
||||
e.downcast_ref::<PrebakeError>()
|
||||
.map(|pe| pe.exit_code())
|
||||
.unwrap_or(1),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Commands::Bake {
|
||||
script,
|
||||
bake_base,
|
||||
prebake,
|
||||
}) => {
|
||||
log::info!("Running in standalone mode.");
|
||||
let mut ctx = ExecutionContext {
|
||||
standalone: true,
|
||||
task_id: format!("{}-{}-bake", cli.pipeline, cli.build_id),
|
||||
pipeline_name: cli.pipeline.clone(),
|
||||
username: cli.username.clone(),
|
||||
dry_run: cli.dry_run,
|
||||
privileged: false,
|
||||
..Default::default()
|
||||
};
|
||||
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
|
||||
let event_tx: EventSender = Some(tx);
|
||||
let pipeline_name = cli.pipeline.clone();
|
||||
let build_id = cli.build_id.clone();
|
||||
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||
event_rx,
|
||||
pipeline_name,
|
||||
build_id,
|
||||
));
|
||||
let result = bake(script, bake_base, prebake, &cli, &mut ctx, event_tx).await;
|
||||
drop(ctx);
|
||||
let _ = receiver_handle.await;
|
||||
dbg!(&result);
|
||||
if result.is_err() {
|
||||
log::error!("Bake stage failed! Exiting...");
|
||||
std::process::exit(1);
|
||||
BareCommands::Bake {
|
||||
script,
|
||||
bake_base,
|
||||
prebake: prebake_path,
|
||||
} => {
|
||||
log::info!("Running in standalone mode.");
|
||||
let mut ctx = ExecutionContext {
|
||||
standalone: true,
|
||||
task_id: format!("{}-{}-bake", cli.pipeline, cli.build_id),
|
||||
pipeline_name: cli.pipeline.clone(),
|
||||
username: cli.username.clone(),
|
||||
dry_run: cli.dry_run,
|
||||
privileged: false,
|
||||
..Default::default()
|
||||
};
|
||||
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
|
||||
let event_tx: EventSender = Some(tx);
|
||||
let pipeline_name = cli.pipeline.clone();
|
||||
let build_id = cli.build_id.clone();
|
||||
let receiver_handle =
|
||||
tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||
event_rx,
|
||||
pipeline_name,
|
||||
build_id,
|
||||
));
|
||||
let result =
|
||||
bake(script, bake_base, prebake_path, &cli, &mut ctx, event_tx).await;
|
||||
drop(ctx);
|
||||
let _ = receiver_handle.await;
|
||||
dbg!(&result);
|
||||
if result.is_err() {
|
||||
log::error!("Bake stage failed! Exiting...");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Commands::Finalize { config }) => {
|
||||
log::info!("Running in standalone mode.");
|
||||
let mut ctx = ExecutionContext {
|
||||
standalone: true,
|
||||
task_id: format!("{}-{}-finalize", cli.pipeline, cli.build_id),
|
||||
pipeline_name: cli.pipeline.clone(),
|
||||
username: cli.username.clone(),
|
||||
dry_run: cli.dry_run,
|
||||
privileged: false,
|
||||
..Default::default()
|
||||
};
|
||||
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
|
||||
let event_tx: EventSender = Some(tx);
|
||||
let pipeline_name = cli.pipeline.clone();
|
||||
let build_id = cli.build_id.clone();
|
||||
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||
event_rx,
|
||||
pipeline_name,
|
||||
build_id,
|
||||
));
|
||||
let result = finalize(Path::new(config), &cli, &mut ctx, event_tx).await;
|
||||
drop(ctx);
|
||||
let _ = receiver_handle.await;
|
||||
dbg!(&result);
|
||||
if let Err(ref e) = result {
|
||||
log::error!("Finalize stage failed: {}", e);
|
||||
std::process::exit(1);
|
||||
BareCommands::Finalize { config } => {
|
||||
log::info!("Running in standalone mode.");
|
||||
let mut ctx = ExecutionContext {
|
||||
standalone: true,
|
||||
task_id: format!("{}-{}-finalize", cli.pipeline, cli.build_id),
|
||||
pipeline_name: cli.pipeline.clone(),
|
||||
username: cli.username.clone(),
|
||||
dry_run: cli.dry_run,
|
||||
privileged: false,
|
||||
..Default::default()
|
||||
};
|
||||
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
|
||||
let event_tx: EventSender = Some(tx);
|
||||
let pipeline_name = cli.pipeline.clone();
|
||||
let build_id = cli.build_id.clone();
|
||||
let receiver_handle =
|
||||
tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||
event_rx,
|
||||
pipeline_name,
|
||||
build_id,
|
||||
));
|
||||
let result =
|
||||
finalize(Path::new(config), &cli, &mut ctx, event_tx).await;
|
||||
drop(ctx);
|
||||
let _ = receiver_handle.await;
|
||||
dbg!(&result);
|
||||
if let Err(ref e) = result {
|
||||
log::error!("Finalize stage failed: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
// Running in daemon mode
|
||||
log::info!("Executor will be running in daemon mode.");
|
||||
|
||||
let _result = daemon::daemon(&cli).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,6 @@ type NotifyEventReceiver = tokio::sync::mpsc::Receiver<BundledNotifyEvent>;
|
||||
pub type NotifyEventSender = tokio::sync::mpsc::Sender<NotifyEvent>;
|
||||
|
||||
const NOTIFY_NOTREADY_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
const NOTIFY_NOTREADY_PRIORITY: u32 = 99;
|
||||
const NOTIFY_TEMPORARY_DELAY_FACTOR: f64 = 2.0;
|
||||
const NOTIFY_TEMPORARY_DELAY_TIME: f64 = 5.0;
|
||||
const NOTIFY_TEMPORARY_MAX_TIME: f64 = f64::MAX;
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::prebake::config::Bootstrap;
|
||||
use crate::prebake::config::PrebakeConfig;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{PathBuf,Path};
|
||||
use which::which;
|
||||
|
||||
/// Executes the bootstrap stage of the prebake workflow.
|
||||
@@ -53,22 +53,28 @@ pub async fn bootstrap(
|
||||
|
||||
let bootstrap_config = config.bootstrap.clone().unwrap_or_default();
|
||||
|
||||
if bootstrap_config.custom.is_none() {
|
||||
let script = generate_bootstrap(&bootstrap_config, config, osinfo)?;
|
||||
if let Some(custom_bootstrap) = bootstrap_config.custom {
|
||||
if ctx.dry_run {
|
||||
log::info!("[DRY_RUN] User provided bootstrap script:\n{}", custom_bootstrap);
|
||||
return Ok(());
|
||||
}
|
||||
// TODO: Parse custom bootstrap URL and download them
|
||||
log::debug!("User provided bootstrap script:\n{}", custom_bootstrap);
|
||||
if ctx.standalone {
|
||||
write_bootstrap_content(&bootstrap_path, custom_bootstrap)?;
|
||||
}
|
||||
else {
|
||||
// Requires PIP(Pipeline Implicit Parameters) to be implemented
|
||||
unimplemented!("Permission check is not implemented, failing...");
|
||||
}
|
||||
} else {
|
||||
let script = generate_bootstrap(&bootstrap_config, config, osinfo, ctx.standalone)?;
|
||||
if ctx.dry_run {
|
||||
log::info!("[DRY_RUN] Generated bootstrap script:\n{}", script);
|
||||
return Ok(());
|
||||
}
|
||||
log::debug!("Generated bootstrap script:\n{}", script);
|
||||
let mut file = File::create(&bootstrap_path)?;
|
||||
file.write_all(script.as_bytes())?;
|
||||
std::fs::set_permissions(
|
||||
&bootstrap_path,
|
||||
std::os::unix::fs::PermissionsExt::from_mode(0o755),
|
||||
)?;
|
||||
} else {
|
||||
// Requires PIP(Pipeline Implicit Parameters) to be implemented
|
||||
unimplemented!("Not supporting custom bootstrap.sh at this time");
|
||||
write_bootstrap_content(&bootstrap_path, script)?;
|
||||
}
|
||||
|
||||
// Check if bootstrap script exists before execution
|
||||
@@ -84,6 +90,16 @@ pub async fn bootstrap(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_bootstrap_content(bootstrap_path: &Path, content: String) -> Result<(), BootstrapError> {
|
||||
let mut file = File::create(bootstrap_path)?;
|
||||
file.write_all(content.as_bytes())?;
|
||||
std::fs::set_permissions(
|
||||
&bootstrap_path,
|
||||
std::os::unix::fs::PermissionsExt::from_mode(0o755),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates a bootstrap script based on configuration and target OS.
|
||||
///
|
||||
/// The generated script performs the following setup tasks:
|
||||
@@ -117,6 +133,7 @@ fn generate_bootstrap(
|
||||
bootstrap_config: &Bootstrap,
|
||||
config: &PrebakeConfig,
|
||||
osinfo: &os_info::Info,
|
||||
standalone: bool,
|
||||
) -> Result<String, BootstrapError> {
|
||||
let mut script = String::new();
|
||||
// Generate header
|
||||
@@ -128,7 +145,12 @@ fn generate_bootstrap(
|
||||
let user = &bootstrap_config.user;
|
||||
if user == "root" {
|
||||
// Requires PIP(Pipeline Implicit Parameters) to be implemented
|
||||
unimplemented!("Permission check is not implemented, failing...");
|
||||
// unimplemented!("Permission check is not implemented, failing...");
|
||||
if standalone {
|
||||
log::warn!("Running the whole pipeline with root could harm your pipeline or cause undefined behavior.")
|
||||
} else {
|
||||
unimplemented!("Permission check is not implemented, failing...");
|
||||
}
|
||||
} else {
|
||||
script.push_str("# User creation\n");
|
||||
script.push_str(&format!("if ! id -u {} >/dev/null 2>&1; then\n", user));
|
||||
@@ -166,8 +188,19 @@ fn generate_bootstrap(
|
||||
script.push_str("# Setup privilege executor\n");
|
||||
let pkg_manager = get_package_manager(config, osinfo);
|
||||
if which("sudo").is_ok() {
|
||||
if bootstrap_config.sudoers.is_some() {
|
||||
unimplemented!("Permission check is not implemented, failing...")
|
||||
if let Some(sudoers) = &bootstrap_config.sudoers {
|
||||
if standalone {
|
||||
log::warn!("Pipeline permission check disabled due to Standalone mode.");
|
||||
// TODO: Introduce permission check
|
||||
} else {
|
||||
// In client mode, custom sudoers is constrainted by PIPELINE_CUSTOM_SUDOERS
|
||||
// TODO: Introduce permission check
|
||||
unimplemented!("Permission check is not implemented, failing...")
|
||||
}
|
||||
script.push_str(&format!(
|
||||
"echo '{}' | tee /etc/sudoers.d/workshop > /dev/null\n",
|
||||
sudoers
|
||||
));
|
||||
}
|
||||
if let Some(pkg_manager) = pkg_manager {
|
||||
let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary());
|
||||
@@ -179,12 +212,29 @@ fn generate_bootstrap(
|
||||
script.push_str("chmod 440 /etc/sudoers.d/workshop\n");
|
||||
}
|
||||
} else if which("doas").is_ok() {
|
||||
if bootstrap_config.doas.is_some() {
|
||||
// Requires PIP(Pipeline Implicit Parameters) to be implemented
|
||||
unimplemented!("Permission check is not implemented, failing...")
|
||||
if let Some(doas) = &bootstrap_config.doas {
|
||||
if standalone {
|
||||
log::warn!("Pipeline permission check disabled due to Standalone mode.");
|
||||
// TODO: Introduce permission check
|
||||
} else {
|
||||
// In client mode, custom sudoers is constrainted by PIPELINE_CUSTOM_SUDOERS
|
||||
// TODO: Introduce permission check
|
||||
unimplemented!("Permission check is not implemented, failing...")
|
||||
}
|
||||
script.push_str(&format!(
|
||||
"cat >> /etc/doas.conf << 'EOF'\n{}\nEOF\n",
|
||||
doas
|
||||
));
|
||||
}
|
||||
if let Some(pkg_manager) = pkg_manager {
|
||||
let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary());
|
||||
let content = format!("permit nopass {} as root cmd {}", user, allowed_commands);
|
||||
script.push_str(&format!(
|
||||
"cat >> /etc/doas.conf << 'EOF'\n{}\nEOF\n",
|
||||
content
|
||||
));
|
||||
script.push_str("chmod 600 /etc/doas.conf\n");
|
||||
}
|
||||
// We do not know how to write doas.conf lol
|
||||
unimplemented!("doas is not implemented");
|
||||
}
|
||||
|
||||
script.push_str("echo 'Bootstrap complete!'\n");
|
||||
|
||||
@@ -6,3 +6,4 @@ pub mod command;
|
||||
pub mod memsize;
|
||||
pub mod repology;
|
||||
pub mod time;
|
||||
pub mod compression;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CompressionMethod {
|
||||
#[default]
|
||||
Zstd,
|
||||
Gzip,
|
||||
None,
|
||||
Dir,
|
||||
}
|
||||
Reference in New Issue
Block a user