diff --git a/workshop-baker/Cargo.toml b/workshop-baker/Cargo.toml index 9df4fb9..71586ea 100644 --- a/workshop-baker/Cargo.toml +++ b/workshop-baker/Cargo.toml @@ -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" diff --git a/workshop-baker/src/finalize/config.rs b/workshop-baker/src/finalize/config.rs index 4c55b75..1e77689 100644 --- a/workshop-baker/src/finalize/config.rs +++ b/workshop-baker/src/finalize/config.rs @@ -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, + + /// 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, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct NotificationTemplate { /// Payload schema: "default" (plugin-defined) or "custom" (requires on_* templates) #[serde(default)] pub schema: Option, @@ -117,10 +145,6 @@ pub struct NotificationMethod { /// Stage-triggered notification configuration #[serde(default)] pub on_finish_of: Option, - - /// 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>, + pub templates: HashMap, - /// Priority groups for notification delivery #[serde(flatten)] pub groups: HashMap>, } @@ -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, + + /// Content template for build success notification + #[serde(default)] + pub on_success: Option, + + /// Content template for build failure notification + #[serde(default)] + pub on_failure: Option, + + /// Content template for stage completion notification + #[serde(default)] + pub on_finish_of: Option, +} + +/// 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>), +} + +/// 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, + + /// 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, - - /// Publishing targets - #[serde(default)] - pub publish: HashMap, + // 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, - - /// 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 { - vec![ArtifactCondition::Success] -} - -fn default_cleanup_policy() -> CleanupPolicy { - CleanupPolicy::Auto -} - impl FinalizeConfig { /// Validate the finalize configuration pub fn validate(&self) -> Result<(), Vec> { @@ -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) { + // 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 + )); + } + } } } diff --git a/workshop-baker/src/lib.rs b/workshop-baker/src/lib.rs index a0b6176..6c587a6 100644 --- a/workshop-baker/src/lib.rs +++ b/workshop-baker/src/lib.rs @@ -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 }, } diff --git a/workshop-baker/src/main.rs b/workshop-baker/src/main.rs index b533398..b94e791 100644 --- a/workshop-baker/src/main.rs +++ b/workshop-baker/src/main.rs @@ -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::() - .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::() + .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; } } diff --git a/workshop-baker/src/notify.rs b/workshop-baker/src/notify.rs index d559a13..8d5d9eb 100644 --- a/workshop-baker/src/notify.rs +++ b/workshop-baker/src/notify.rs @@ -93,7 +93,6 @@ type NotifyEventReceiver = tokio::sync::mpsc::Receiver; pub type NotifyEventSender = tokio::sync::mpsc::Sender; 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; diff --git a/workshop-baker/src/prebake/stage/bootstrap.rs b/workshop-baker/src/prebake/stage/bootstrap.rs index 41a7a92..862995b 100644 --- a/workshop-baker/src/prebake/stage/bootstrap.rs +++ b/workshop-baker/src/prebake/stage/bootstrap.rs @@ -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 { 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"); diff --git a/workshop-baker/src/types.rs b/workshop-baker/src/types.rs index a963889..36c2dc8 100644 --- a/workshop-baker/src/types.rs +++ b/workshop-baker/src/types.rs @@ -6,3 +6,4 @@ pub mod command; pub mod memsize; pub mod repology; pub mod time; +pub mod compression; diff --git a/workshop-baker/src/types/compression.rs b/workshop-baker/src/types/compression.rs new file mode 100644 index 0000000..704863a --- /dev/null +++ b/workshop-baker/src/types/compression.rs @@ -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, +}