Compare commits

...

4 Commits

Author SHA1 Message Date
Catty Steve 9e45bdbbfb refactor(notify): rename NotifyEvent to NotificationEvent
CI / Check & Lint (push) Failing after 19m30s
2026-04-24 23:10:04 +08:00
Catty Steve 5ce8b72900 refactor(notify): extract types to separate module and redesign
NotificationRule

- Move NotifyEvent, NotifyPluginMap, and related types to
  notify/types.rs
- Introduce NotificationRule struct replacing NotificationMethod fields
- Add NotificationRulesMap for priority-based notification configuration
- Comment out main event loop and add todo!() placeholders for WIP
2026-04-24 23:01:40 +08:00
Catty Steve 2c9d32e954 refactor(error): extract exit code logic into HasExitCode trait
Introduce a common HasExitCode trait for PrebakeError, BakeError,
ExecutionError, and FinalizeError. Refactor main.rs to consolidate
CLI command handling and use the trait for unified exit code resolution.
2026-04-24 21:45:57 +08:00
Catty Steve 901005639b 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
2026-04-24 21:12:10 +08:00
12 changed files with 551 additions and 434 deletions
+117
View File
@@ -0,0 +1,117 @@
diff --git a/workshop-baker/src/notify.rs b/workshop-baker/src/notify.rs
index 75bce82..ad56c66 100644
--- a/workshop-baker/src/notify.rs
+++ b/workshop-baker/src/notify.rs
@@ -3,15 +3,15 @@ use crate::finalize::FinalizeConfig;
use crate::finalize::parse;
use crate::finalize::plugin;
use crate::finalize::plugin::fetch;
-use crate::notify::types::BundledNotifyEvent;
+use crate::notify::types::BundledNotificationEvent;
use crate::notify::types::NOTIFY_TEMPORARY_DELAY_FACTOR;
use crate::notify::types::NOTIFY_TEMPORARY_DELAY_TIME;
use crate::notify::types::NOTIFY_TEMPORARY_MAX_TIME;
use crate::notify::types::NotificationRulesMap;
-use crate::notify::types::NotifyEvent;
-use crate::notify::types::NotifyEventReceiver;
-use crate::notify::types::NotifyEventSender;
-use crate::notify::types::NotifyPluginMap;
+use crate::notify::types::NotificationEvent;
+use crate::notify::types::NotificationEventReceiver;
+use crate::notify::types::NotificationEventSender;
+use crate::notify::types::NotificationPluginMap;
use crate::{EventSender, ExecutionContext};
use std::collections::HashSet;
use std::path::Path;
@@ -46,8 +46,8 @@ pub async fn notify(
finalize_path: &Path,
ctx: &mut ExecutionContext,
event_tx: EventSender,
- notifyevent_rx: &mut NotifyEventReceiver,
- notifyevent_tx: NotifyEventSender,
+ notifyevent_rx: &mut NotificationEventReceiver,
+ notifyevent_tx: NotificationEventSender,
) -> anyhow::Result<()> {
let mut finalize = parse(finalize_path)?;
validate(&finalize)?;
@@ -114,10 +114,11 @@ pub async fn notify(
}
fn notification_handler(
- plugins: &NotifyPluginMap,
+ plugins: &NotificationPluginMap,
config: &NotificationRulesMap,
- event: &BundledNotifyEvent,
+ event: &BundledNotificationEvent,
) -> Result<(), NotifyError> {
+
todo!();
}
@@ -132,7 +133,7 @@ fn wait_time(group_period: u32, group_watermark: u32, count: u32) -> f32 {
}
// calculate p^(m-l)*t as notification delay
-fn notify_delay(event: &NotifyEvent) -> std::time::Duration {
+fn notify_delay(event: &NotificationEvent) -> std::time::Duration {
let exponent = (event.max_lives - event.lives) as i32;
if exponent < 0 {
log::warn!(
diff --git a/workshop-baker/src/notify/types.rs b/workshop-baker/src/notify/types.rs
index b852dfb..d6ccfb7 100644
--- a/workshop-baker/src/notify/types.rs
+++ b/workshop-baker/src/notify/types.rs
@@ -39,13 +39,13 @@ pub struct NotificationRule{
pub type Priority = u32;
pub type Method = String;
pub type NotificationRulesMap = HashMap<Priority, HashMap<Method, Vec<NotificationRule>>>;
-pub type BundledNotifyEvent = Vec<NotifyEvent>;
-pub type NotifyEventReceiver = tokio::sync::mpsc::Receiver<BundledNotifyEvent>;
-pub type NotifyEventSender = tokio::sync::mpsc::Sender<NotifyEvent>;
-pub type NotifyPluginMap = HashMap<String, FinalizePlugin>;
+pub type BundledNotificationEvent = Vec<NotificationEvent>;
+pub type NotificationEventReceiver = tokio::sync::mpsc::Receiver<BundledNotificationEvent>;
+pub type NotificationEventSender = tokio::sync::mpsc::Sender<NotificationEvent>;
+pub type NotificationPluginMap = HashMap<String, FinalizePlugin>;
#[derive(Debug, Clone, Serialize)]
-pub struct NotifyEvent {
+pub struct NotificationEvent {
pub id: Uuid,
pub stage: StagePhase,
pub substage: String,
@@ -58,21 +58,21 @@ pub struct NotifyEvent {
pub target: HashSet<String>, // Target audience
}
-impl Hash for NotifyEvent {
+impl Hash for NotificationEvent {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
-impl PartialEq for NotifyEvent {
+impl PartialEq for NotificationEvent {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
-impl Eq for NotifyEvent {}
+impl Eq for NotificationEvent {}
-impl PartialOrd for NotifyEvent {
+impl PartialOrd for NotificationEvent {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(
self.effective_priority
@@ -83,7 +83,7 @@ impl PartialOrd for NotifyEvent {
}
}
-impl Ord for NotifyEvent {
+impl Ord for NotificationEvent {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap()
}
+1 -1
View File
@@ -24,7 +24,7 @@ anyhow = "1.0.100"
bollard = { version = "0.20.0", features = ["buildkit", "chrono", "ssh"] } bollard = { version = "0.20.0", features = ["buildkit", "chrono", "ssh"] }
cgroups-rs = "0.5.0" cgroups-rs = "0.5.0"
chrono = { version = "0.4.42", features = ["serde"] } 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" config = "0.15.19"
duration-str = "0.21.0" duration-str = "0.21.0"
env_logger = "0.11.8" env_logger = "0.11.8"
+26 -4
View File
@@ -1,7 +1,14 @@
use os_info::Info; use os_info::Info;
use std::fmt::Debug;
use std::fmt::Display;
use std::path::PathBuf; use std::path::PathBuf;
use thiserror::Error; use thiserror::Error;
// Obsolete?
pub trait HasExitCode: Display + Debug + Send + Sync + Sized + 'static {
fn exit_code(&self) -> i32;
}
pub const EXITCODE_OK: i32 = 0; pub const EXITCODE_OK: i32 = 0;
pub const EXITCODE_GENERAL_ERROR: i32 = 1; pub const EXITCODE_GENERAL_ERROR: i32 = 1;
pub const EXITCODE_INVALID_ARGS: i32 = 2; pub const EXITCODE_INVALID_ARGS: i32 = 2;
@@ -69,8 +76,8 @@ pub enum PrebakeError {
ConfigError(String), ConfigError(String),
} }
impl PrebakeError { impl HasExitCode for PrebakeError {
pub fn exit_code(&self) -> i32 { fn exit_code(&self) -> i32 {
match self { match self {
PrebakeError::PrivDropFailed(_) => EXITCODE_PRIV_DROP_FAILED, PrebakeError::PrivDropFailed(_) => EXITCODE_PRIV_DROP_FAILED,
PrebakeError::UserNotFound(_) => EXITCODE_PRIV_DROP_FAILED, PrebakeError::UserNotFound(_) => EXITCODE_PRIV_DROP_FAILED,
@@ -149,8 +156,23 @@ pub enum ExecutionError {
PackageManagerNotFound, PackageManagerNotFound,
} }
impl ExecutionError { impl HasExitCode for BakeError {
pub fn exit_code(&self) -> i32 { fn exit_code(&self) -> i32 {
match self {
BakeError::ScriptGenerationFailed { .. } => EXITCODE_IO_ERROR,
BakeError::TemplateError(_) => EXITCODE_PARSE_ERROR,
BakeError::CircularDependency(_) => EXITCODE_PARSE_ERROR,
BakeError::UnknownDependency(..) => EXITCODE_PARSE_ERROR,
BakeError::IoError(_) => EXITCODE_IO_ERROR,
BakeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
BakeError::DecoratorParseError { .. } => EXITCODE_PARSE_ERROR,
BakeError::ScriptExecutionError(e) => e.exit_code(),
}
}
}
impl HasExitCode for ExecutionError {
fn exit_code(&self) -> i32 {
match self { match self {
ExecutionError::InvalidCommand(_) => EXITCODE_EXECUTION_ERROR, ExecutionError::InvalidCommand(_) => EXITCODE_EXECUTION_ERROR,
ExecutionError::ExecutionFailed(_) => EXITCODE_EXECUTION_ERROR, ExecutionError::ExecutionFailed(_) => EXITCODE_EXECUTION_ERROR,
+98 -200
View File
@@ -4,6 +4,7 @@
//! which handles artifact collection, publishing, and notifications. //! which handles artifact collection, publishing, and notifications.
use crate::types::command::CustomCommand; use crate::types::command::CustomCommand;
use crate::types::compression::CompressionMethod;
use crate::types::time::deserialize_duration_ms; use crate::types::time::deserialize_duration_ms;
use semver::{Version, VersionReq}; use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -98,10 +99,25 @@ pub struct PluginRuntimeConfig {
/// Notification method configuration /// Notification method configuration
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct NotificationMethod { pub struct NotificationMethod {
/// Whether notification failure should not trigger next priority group /// Template definition
#[serde(default)] #[serde(default)]
pub fallible: bool, 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) /// Payload schema: "default" (plugin-defined) or "custom" (requires on_* templates)
#[serde(default)] #[serde(default)]
pub schema: Option<String>, pub schema: Option<String>,
@@ -117,10 +133,6 @@ pub struct NotificationMethod {
/// Stage-triggered notification configuration /// Stage-triggered notification configuration
#[serde(default)] #[serde(default)]
pub on_finish_of: Option<OnFinishOf>, pub on_finish_of: Option<OnFinishOf>,
/// Notification-specific configuration (url, token, to, from, etc.)
#[serde(flatten)]
pub config: Value,
} }
/// Notification configuration container /// Notification configuration container
@@ -128,9 +140,8 @@ pub struct NotificationMethod {
pub struct NotificationConfig { pub struct NotificationConfig {
/// Template definitions for notification content /// Template definitions for notification content
#[serde(default)] #[serde(default)]
pub templates: HashMap<String, HashMap<String, PluginConfig>>, pub templates: HashMap<String, NotificationTemplateDef>,
/// Priority groups for notification delivery
#[serde(flatten)] #[serde(flatten)]
pub groups: HashMap<u32, HashMap<String, NotificationMethod>>, pub groups: HashMap<u32, HashMap<String, NotificationMethod>>,
} }
@@ -146,6 +157,63 @@ pub struct OnFinishOf {
pub content: String, 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 /// Artifact definition
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ArtifactDef { pub struct ArtifactDef {
@@ -167,16 +235,10 @@ pub struct ArtifactDef {
pub retention_ms: u64, pub retention_ms: u64,
/// Compression method: "zstd", "gzip", "none", "dir" /// Compression method: "zstd", "gzip", "none", "dir"
#[serde(default = "default_compression")]
pub compression: CompressionMethod,
/// Trigger conditions: "success", "failure", "always" (WIP: failure, always)
#[serde(default = "default_on_conditions")]
pub on: Vec<ArtifactCondition>,
/// Publishing targets
#[serde(default)] #[serde(default)]
pub publish: HashMap<String, PublishTarget>, pub compression: CompressionMethod,
// Publishing targets
// TODO: under heavy refactor
} }
impl Default for ArtifactDef { impl Default for ArtifactDef {
@@ -185,55 +247,19 @@ impl Default for ArtifactDef {
id: None, id: None,
path: String::new(), path: String::new(),
retention_ms: default_retention_ms(), retention_ms: default_retention_ms(),
compression: default_compression(), compression: CompressionMethod::default(),
on: default_on_conditions(),
publish: HashMap::new(),
} }
} }
} }
/// Compression method for artifacts /// Artifact
#[derive(Debug, Deserialize, Serialize, Clone, Default)] /// TODO: Under heavy refactor
#[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,
}
/// Cleanup configuration /// Cleanup configuration
#[derive(Debug, Deserialize, Serialize, Clone, Default)] #[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct CleanupConfig { pub struct CleanupConfig {
/// Cleanup policy: "auto", "manual", "on_failure" (WIP: manual, on_failure) /// Cleanup policy: "auto", "manual", "on_failure" (WIP: manual, on_failure)
#[serde(default = "default_cleanup_policy")] #[serde(default)]
pub policy: CleanupPolicy, pub policy: CleanupPolicy,
} }
@@ -244,7 +270,8 @@ pub enum CleanupPolicy {
#[default] #[default]
Auto, Auto,
Manual, Manual,
OnFailure, OnSuccess,
Never,
} }
/// Hooks configuration for finalize stage /// Hooks configuration for finalize stage
@@ -264,18 +291,6 @@ fn default_retention_ms() -> u64 {
7 * 24 * 3600 * 1000 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 { impl FinalizeConfig {
/// Validate the finalize configuration /// Validate the finalize configuration
pub fn validate(&self) -> Result<(), Vec<String>> { pub fn validate(&self) -> Result<(), Vec<String>> {
@@ -311,8 +326,7 @@ impl FinalizeConfig {
} }
} }
// TODO: Schema-based validation for plugin configs self.validate_notification(&mut errors);
// TODO: Validate that publish targets reference existing plugins
if errors.is_empty() { if errors.is_empty() {
Ok(()) Ok(())
@@ -320,136 +334,20 @@ impl FinalizeConfig {
Err(errors) Err(errors)
} }
} }
}
#[cfg(test)] fn validate_notification(&self, errors: &mut Vec<String>) {
mod tests { // Validate templates
use super::*; for (name, tmpl) in &self.notification.templates {
if tmpl.use_.is_some()
#[test] && (tmpl.on_success.is_some()
fn test_deserialize_hooks_full() { || tmpl.on_failure.is_some()
let yaml = r#" || tmpl.on_finish_of.is_some())
version: "0.0.1" {
hooks: errors.push(format!(
early: "Template '{}' cannot combine 'use' with 'on_*' fields (MVP restriction)",
- name: "test-early" name
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"))
);
} }
} }
+3 -2
View File
@@ -5,6 +5,7 @@ use crate::{
ExecutionError, ExecutionError,
error::{ error::{
EXITCODE_EXECUTION_ERROR, EXITCODE_GENERAL_ERROR, EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR, EXITCODE_EXECUTION_ERROR, EXITCODE_GENERAL_ERROR, EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR,
HasExitCode,
}, },
}; };
@@ -38,8 +39,8 @@ pub enum FinalizeError {
ExecutionError(#[from] ExecutionError), ExecutionError(#[from] ExecutionError),
} }
impl FinalizeError { impl HasExitCode for FinalizeError {
pub fn exit_code(&self) -> i32 { fn exit_code(&self) -> i32 {
match self { match self {
FinalizeError::YamlParseError(_) => EXITCODE_PARSE_ERROR, FinalizeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
FinalizeError::ValidateError(_) => EXITCODE_PARSE_ERROR, FinalizeError::ValidateError(_) => EXITCODE_PARSE_ERROR,
+18 -8
View File
@@ -34,19 +34,19 @@ pub mod cli {
#[command(version, about, long_about=None)] #[command(version, about, long_about=None)]
pub struct Cli { pub struct Cli {
/// Username of the pipeline executor /// Username of the pipeline executor
#[arg(short, long)] #[arg(short, long, env = "HBW_USERNAME")]
pub username: String, pub username: String,
/// Pipeline ID of this pipeline /// Pipeline ID of this pipeline
#[arg(short, long)] #[arg(short, long, env = "HBW_PIPELINE")]
pub pipeline: String, pub pipeline: String,
/// Build ID of this build /// Build ID of this build
#[arg(short, long)] #[arg(short, long, env = "HBW_BUILD_ID")]
pub build_id: String, pub build_id: String,
/// Parse the script without executing /// Parse the script without executing
#[arg(long, default_value = "false")] #[arg(long, default_value = "false", env = "HBW_DRY_RUN")]
pub dry_run: bool, pub dry_run: bool,
#[command(subcommand)] #[command(subcommand)]
@@ -55,12 +55,21 @@ pub mod cli {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
pub enum Commands { pub enum Commands {
/// Monitor a running task /// Run pipeline stages in standalone (bare) mode
Monitor { group: String }, Bare {
/// Prepare a task #[command(subcommand)]
Prebake { config: PathBuf }, command: BareCommands,
},
/// Run in client mode /// Run in client mode
Client { config: String }, Client { config: String },
}
#[derive(Subcommand, Debug)]
pub enum BareCommands {
/// Prepare a task
Prebake { config: PathBuf },
/// Run a task /// Run a task
Bake { Bake {
/// Path to the script to execute /// Path to the script to execute
@@ -71,6 +80,7 @@ pub mod cli {
#[arg(short, long, default_value = "./prebake.yml")] #[arg(short, long, default_value = "./prebake.yml")]
prebake: PathBuf, prebake: PathBuf,
}, },
/// Finish a task /// Finish a task
Finalize { config: String }, Finalize { config: String },
} }
+44 -80
View File
@@ -2,10 +2,10 @@ use std::path::Path;
use workshop_baker::{ use workshop_baker::{
bake::bake, bake::bake,
cli::{Cli, Commands, Parser}, cli::{BareCommands, Cli, Commands, Parser},
daemon, daemon,
engine::{EventSender, ExecutionContext}, engine::{EventSender, ExecutionContext},
error::PrebakeError, error::HasExitCode,
finalize::finalize, finalize::finalize,
prebake::prebake, prebake::prebake,
}; };
@@ -18,111 +18,75 @@ async fn main() {
log::warn!("Dry run enabled, no changes will be made."); log::warn!("Dry run enabled, no changes will be made.");
} }
match &cli.command { match &cli.command {
Some(Commands::Monitor { group: _ }) => {
unimplemented!("executor-monitor should be moved to daemon instead.")
}
Some(Commands::Client { config: _ }) => { Some(Commands::Client { config: _ }) => {
unimplemented!("Client mode not yet implemented.") unimplemented!("Client mode not yet implemented.")
} }
Some(Commands::Prebake { config }) => { Some(Commands::Bare { command }) => {
log::info!("Running in standalone mode."); log::info!("Running in standalone mode.");
let (stage, privileged) = match command {
BareCommands::Prebake { .. } => ("init", true),
BareCommands::Bake { .. } => ("bake", false),
BareCommands::Finalize { .. } => ("finalize", false),
};
let mut ctx = ExecutionContext { let mut ctx = ExecutionContext {
standalone: true, standalone: true,
task_id: format!("{}-{}-init", cli.pipeline, cli.build_id), task_id: format!("{}-{}-{}", cli.pipeline, cli.build_id, stage),
pipeline_name: cli.pipeline.clone(), pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(), username: cli.username.clone(),
dry_run: cli.dry_run, dry_run: cli.dry_run,
privileged: true, privileged,
..Default::default() ..Default::default()
}; };
let (tx, event_rx) = tokio::sync::mpsc::channel(256); let (tx, event_rx) = tokio::sync::mpsc::channel(256);
let event_tx: EventSender = Some(tx); let event_tx: EventSender = Some(tx);
let pipeline_name = cli.pipeline.clone(); let handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
let build_id = cli.build_id.clone();
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
event_rx, event_rx,
pipeline_name, cli.pipeline.clone(),
build_id, cli.build_id.clone(),
)); ));
let result = prebake(config, &cli, None, &mut ctx, event_tx).await;
drop(ctx); let result: anyhow::Result<()> = match command {
let _ = receiver_handle.await; BareCommands::Prebake { config } => {
dbg!(&result); prebake(config, &cli, None, &mut ctx, event_tx).await
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),
);
} }
} BareCommands::Bake {
Some(Commands::Bake {
script, script,
bake_base, bake_base,
prebake, prebake: prebake_path,
}) => { } => bake(script, bake_base, prebake_path, &cli, &mut ctx, event_tx)
log::info!("Running in standalone mode."); .await
let mut ctx = ExecutionContext { .map_err(|e| e.into()),
standalone: true, BareCommands::Finalize { config } => {
task_id: format!("{}-{}-bake", cli.pipeline, cli.build_id), finalize(Path::new(config), &cli, &mut ctx, event_tx)
pipeline_name: cli.pipeline.clone(), .await
username: cli.username.clone(), .map_err(|e| e.into())
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);
} }
}
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); drop(ctx);
let _ = receiver_handle.await; let _ = handle.await;
dbg!(&result); dbg!(&result);
if let Err(ref e) = result { if let Err(ref e) = result {
log::error!("Finalize stage failed: {}", e); let code = match command {
std::process::exit(1); BareCommands::Prebake { .. } => e
.downcast_ref::<workshop_baker::error::PrebakeError>()
.map(|pe| pe.exit_code())
.unwrap_or(1),
BareCommands::Bake { .. } => e
.downcast_ref::<workshop_baker::error::BakeError>()
.map(|be| be.exit_code())
.unwrap_or(1),
BareCommands::Finalize { .. } => e
.downcast_ref::<workshop_baker::finalize::FinalizeError>()
.map(|fe| fe.exit_code())
.unwrap_or(1),
};
log::error!("{} stage failed: {}", stage, e);
std::process::exit(code);
} }
} }
None => { None => {
// Running in daemon mode
log::info!("Executor will be running in daemon mode."); log::info!("Executor will be running in daemon mode.");
let _result = daemon::daemon(&cli).await; let _result = daemon::daemon(&cli).await;
} }
} }
+60 -114
View File
@@ -1,23 +1,24 @@
use crate::constant::FINALIZE_STANDALONE_PLUGIN_PATH; use crate::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
use crate::finalize::FinalizeConfig; use crate::finalize::FinalizeConfig;
use crate::finalize::NotificationConfig;
use crate::finalize::parse; use crate::finalize::parse;
use crate::finalize::plugin; use crate::finalize::plugin;
use crate::finalize::plugin::FinalizePlugin;
use crate::finalize::plugin::fetch; use crate::finalize::plugin::fetch;
use crate::types::buildstatus::StagePhase; use crate::notify::types::BundledNotificationEvent;
use crate::types::buildstatus::StageResult; use crate::notify::types::NOTIFY_TEMPORARY_DELAY_FACTOR;
use crate::notify::types::NOTIFY_TEMPORARY_DELAY_TIME;
use crate::notify::types::NOTIFY_TEMPORARY_MAX_TIME;
use crate::notify::types::NotificationRulesMap;
use crate::notify::types::NotificationEvent;
use crate::notify::types::NotificationEventReceiver;
use crate::notify::types::NotificationEventSender;
use crate::notify::types::NotificationPluginMap;
use crate::{EventSender, ExecutionContext}; use crate::{EventSender, ExecutionContext};
use chrono::{DateTime, Utc};
use serde::Serialize;
use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
use std::hash::Hash;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use uuid::Uuid;
mod error; mod error;
mod types;
use error::NotifyError; use error::NotifyError;
fn validate(finalize: &FinalizeConfig) -> anyhow::Result<()> { fn validate(finalize: &FinalizeConfig) -> anyhow::Result<()> {
@@ -31,8 +32,6 @@ fn validate(finalize: &FinalizeConfig) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
pub type NotifyPluginMap = HashMap<String, FinalizePlugin>;
fn extract_plugin(finalize: &FinalizeConfig) -> HashSet<String> { fn extract_plugin(finalize: &FinalizeConfig) -> HashSet<String> {
finalize finalize
.notification .notification
@@ -43,67 +42,12 @@ fn extract_plugin(finalize: &FinalizeConfig) -> HashSet<String> {
.collect() .collect()
} }
#[derive(Debug, Clone, Serialize)]
pub struct NotifyEvent {
id: Uuid,
stage: StagePhase,
substage: String,
result: StageResult,
not_before: DateTime<Utc>,
priority: u32,
effective_priority: u32,
lives: u32,
max_lives: u32,
target: HashSet<String>, // Target audience
}
impl Hash for NotifyEvent {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl PartialEq for NotifyEvent {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for NotifyEvent {}
impl PartialOrd for NotifyEvent {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(
self.effective_priority
.cmp(&other.effective_priority)
.then(self.not_before.cmp(&other.not_before))
.reverse(),
)
}
}
impl Ord for NotifyEvent {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap()
}
}
type BundledNotifyEvent = Vec<NotifyEvent>;
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;
pub async fn notify( pub async fn notify(
finalize_path: &Path, finalize_path: &Path,
ctx: &mut ExecutionContext, ctx: &mut ExecutionContext,
event_tx: EventSender, event_tx: EventSender,
notifyevent_rx: &mut NotifyEventReceiver, notifyevent_rx: &mut NotificationEventReceiver,
notifyevent_tx: NotifyEventSender, notifyevent_tx: NotificationEventSender,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut finalize = parse(finalize_path)?; let mut finalize = parse(finalize_path)?;
validate(&finalize)?; validate(&finalize)?;
@@ -125,54 +69,56 @@ pub async fn notify(
} }
let notify_plugins = plugin::register(finalize.plugin, Some(&plugin_list))?; let notify_plugins = plugin::register(finalize.plugin, Some(&plugin_list))?;
loop { // loop {
tokio::select! { // tokio::select! {
Some(events) = notifyevent_rx.recv() => { // Some(events) = notifyevent_rx.recv() => {
match notify_handler(&notify_plugins, &finalize.notification, &events){ // match notify_handler(&notify_plugins, &finalize.notification, &events){
Ok(_) => {}, // Notification succeeded / failed with fallible // Ok(_) => {}, // Notification succeeded / failed with fallible
Err(e) => { // Err(e) => {
log::debug!("Failed to send notification: {:?}", e); // log::debug!("Failed to send notification: {:?}", e);
match e { // match e {
NotifyError::PluginNotAvailable(_) => { // NotifyError::PluginNotAvailable(_) => {
// Should keep lives, add static delay and give less priority // // Should keep lives, add static delay and give less priority
for mut event in events { // for mut event in events {
event.not_before = Utc::now() + NOTIFY_NOTREADY_DELAY; // event.not_before = Utc::now() + NOTIFY_NOTREADY_DELAY;
event.effective_priority = event.effective_priority.saturating_add(1); // event.effective_priority = event.effective_priority.saturating_add(1);
notifyevent_tx.send(event).await?; // notifyevent_tx.send(event).await?;
} // }
}, // },
NotifyError::TemporaryError(_) => { // NotifyError::TemporaryError(_) => {
// Should cost 1 life, add dynamic (exponential) delay // // Should cost 1 life, add dynamic (exponential) delay
for mut event in events { // for mut event in events {
event.lives -= 1; // event.lives -= 1;
event.not_before = Utc::now() + notify_delay(&event); // event.not_before = Utc::now() + notify_delay(&event);
notifyevent_tx.send(event).await?; // notifyevent_tx.send(event).await?;
} // }
}, // },
NotifyError::PermanentError(_) => { // NotifyError::PermanentError(_) => {
// Unexpected! // // Unexpected!
// Should be dropped // // Should be dropped
log::error!("None of the configured notification plugins works."); // log::error!("None of the configured notification plugins works.");
log::error!("Message will be dropped."); // log::error!("Message will be dropped.");
// TODO: Send audit/metric information // // TODO: Send audit/metric information
} // }
error => { // error => {
// Unexpected! // // Unexpected!
log::error!("Internal error: notify_handler returned unexpected error: {:?}", error); // log::error!("Internal error: notify_handler returned unexpected error: {:?}", error);
} // }
}; // };
} // }
}; // };
} // }
} // }
} // }
todo!();
} }
fn notify_handler( fn notification_handler(
plugins: &NotifyPluginMap, plugins: &NotificationPluginMap,
config: &NotificationConfig, config: &NotificationRulesMap,
event: &Vec<NotifyEvent>, event: &BundledNotificationEvent,
) -> Result<(), NotifyError> { ) -> Result<(), NotifyError> {
todo!(); todo!();
} }
@@ -187,7 +133,7 @@ fn wait_time(group_period: u32, group_watermark: u32, count: u32) -> f32 {
} }
// calculate p^(m-l)*t as notification delay // calculate p^(m-l)*t as notification delay
fn notify_delay(event: &NotifyEvent) -> std::time::Duration { fn notify_delay(event: &NotificationEvent) -> std::time::Duration {
let exponent = (event.max_lives - event.lives) as i32; let exponent = (event.max_lives - event.lives) as i32;
if exponent < 0 { if exponent < 0 {
log::warn!( log::warn!(
+96
View File
@@ -0,0 +1,96 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::time::Duration;
use crate::finalize::plugin::FinalizePlugin;
use crate::types::buildstatus::StagePhase;
use crate::types::buildstatus::StageResult;
use chrono::{DateTime, Utc};
use serde::Serialize;
use std::hash::Hash;
use uuid::Uuid;
use crate::finalize::NotificationTemplate;
use crate::finalize::config::Trigger;
/// Final runtime configuration for notification, restored and flattened from yaml
pub struct NotificationRule{
pub enabled: bool,
pub trigger: Vec<Trigger>,
pub template: NotificationTemplate,
/// Whether notification failure should not trigger next priority group
pub fallible: bool,
/// Maximum number of attempts
pub max_lives: u32,
/// Timeout for notification execution
pub timeout: Duration,
/// Plugin-specific configuration
pub config: serde_yaml::Value,
/// Factor of exponential backoff for retry delay
// Not provided for external use
pub retry_backoff: f64,
}
pub type Priority = u32;
pub type Method = String;
pub type NotificationRulesMap = HashMap<Priority, HashMap<Method, Vec<NotificationRule>>>;
pub type BundledNotificationEvent = Vec<NotificationEvent>;
pub type NotificationEventReceiver = tokio::sync::mpsc::Receiver<BundledNotificationEvent>;
pub type NotificationEventSender = tokio::sync::mpsc::Sender<NotificationEvent>;
pub type NotificationPluginMap = HashMap<String, FinalizePlugin>;
#[derive(Debug, Clone, Serialize)]
pub struct NotificationEvent {
pub id: Uuid,
pub stage: StagePhase,
pub substage: String,
pub result: StageResult,
pub not_before: DateTime<Utc>,
pub priority: u32,
pub effective_priority: u32,
pub lives: u32,
pub max_lives: u32,
pub target: HashSet<String>, // Target audience
}
impl Hash for NotificationEvent {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl PartialEq for NotificationEvent {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for NotificationEvent {}
impl PartialOrd for NotificationEvent {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(
self.effective_priority
.cmp(&other.effective_priority)
.then(self.not_before.cmp(&other.not_before))
.reverse(),
)
}
}
impl Ord for NotificationEvent {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap()
}
}
pub const NOTIFY_NOTREADY_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
pub const NOTIFY_TEMPORARY_DELAY_FACTOR: f64 = 2.0;
pub const NOTIFY_TEMPORARY_DELAY_TIME: f64 = 5.0;
pub const NOTIFY_TEMPORARY_MAX_TIME: f64 = f64::MAX;
+68 -17
View File
@@ -16,7 +16,7 @@ use crate::prebake::config::Bootstrap;
use crate::prebake::config::PrebakeConfig; use crate::prebake::config::PrebakeConfig;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use which::which; use which::which;
/// Executes the bootstrap stage of the prebake workflow. /// Executes the bootstrap stage of the prebake workflow.
@@ -53,22 +53,30 @@ pub async fn bootstrap(
let bootstrap_config = config.bootstrap.clone().unwrap_or_default(); let bootstrap_config = config.bootstrap.clone().unwrap_or_default();
if bootstrap_config.custom.is_none() { if let Some(custom_bootstrap) = bootstrap_config.custom {
let script = generate_bootstrap(&bootstrap_config, config, osinfo)?; 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 { if ctx.dry_run {
log::info!("[DRY_RUN] Generated bootstrap script:\n{}", script); log::info!("[DRY_RUN] Generated bootstrap script:\n{}", script);
return Ok(()); return Ok(());
} }
log::debug!("Generated bootstrap script:\n{}", script); log::debug!("Generated bootstrap script:\n{}", script);
let mut file = File::create(&bootstrap_path)?; write_bootstrap_content(&bootstrap_path, script)?;
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");
} }
// Check if bootstrap script exists before execution // Check if bootstrap script exists before execution
@@ -84,6 +92,16 @@ pub async fn bootstrap(
Ok(()) 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. /// Generates a bootstrap script based on configuration and target OS.
/// ///
/// The generated script performs the following setup tasks: /// The generated script performs the following setup tasks:
@@ -117,6 +135,7 @@ fn generate_bootstrap(
bootstrap_config: &Bootstrap, bootstrap_config: &Bootstrap,
config: &PrebakeConfig, config: &PrebakeConfig,
osinfo: &os_info::Info, osinfo: &os_info::Info,
standalone: bool,
) -> Result<String, BootstrapError> { ) -> Result<String, BootstrapError> {
let mut script = String::new(); let mut script = String::new();
// Generate header // Generate header
@@ -128,7 +147,14 @@ fn generate_bootstrap(
let user = &bootstrap_config.user; let user = &bootstrap_config.user;
if user == "root" { if user == "root" {
// Requires PIP(Pipeline Implicit Parameters) to be implemented // Requires PIP(Pipeline Implicit Parameters) to be implemented
// 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..."); unimplemented!("Permission check is not implemented, failing...");
}
} else { } else {
script.push_str("# User creation\n"); script.push_str("# User creation\n");
script.push_str(&format!("if ! id -u {} >/dev/null 2>&1; then\n", user)); script.push_str(&format!("if ! id -u {} >/dev/null 2>&1; then\n", user));
@@ -166,9 +192,20 @@ fn generate_bootstrap(
script.push_str("# Setup privilege executor\n"); script.push_str("# Setup privilege executor\n");
let pkg_manager = get_package_manager(config, osinfo); let pkg_manager = get_package_manager(config, osinfo);
if which("sudo").is_ok() { if which("sudo").is_ok() {
if bootstrap_config.sudoers.is_some() { 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...") 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 { if let Some(pkg_manager) = pkg_manager {
let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary()); let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary());
let content = format!("{} ALL=(ALL:ALL) NOPASSWD {}", user, allowed_commands); let content = format!("{} ALL=(ALL:ALL) NOPASSWD {}", user, allowed_commands);
@@ -179,12 +216,26 @@ fn generate_bootstrap(
script.push_str("chmod 440 /etc/sudoers.d/workshop\n"); script.push_str("chmod 440 /etc/sudoers.d/workshop\n");
} }
} else if which("doas").is_ok() { } else if which("doas").is_ok() {
if bootstrap_config.doas.is_some() { if let Some(doas) = &bootstrap_config.doas {
// Requires PIP(Pipeline Implicit Parameters) to be implemented 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...") unimplemented!("Permission check is not implemented, failing...")
} }
// We do not know how to write doas.conf lol script.push_str(&format!("cat >> /etc/doas.conf << 'EOF'\n{}\nEOF\n", doas));
unimplemented!("doas is not implemented"); }
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");
}
} }
script.push_str("echo 'Bootstrap complete!'\n"); script.push_str("echo 'Bootstrap complete!'\n");
+1
View File
@@ -3,6 +3,7 @@ pub mod builderconfig;
pub mod buildstatus; pub mod buildstatus;
pub mod cache; pub mod cache;
pub mod command; pub mod command;
pub mod compression;
pub mod memsize; pub mod memsize;
pub mod repology; pub mod repology;
pub mod time; pub mod time;
+11
View File
@@ -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,
}